Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03d7d7b0cc | ||
![]() |
f417732942 | ||
![]() |
00edee1cb3 |
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
export TODO_FILE=$PWD/TODO
|
5
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
TODO.bak
|
||||
*.mp4
|
||||
channels/index.html
|
||||
node_modules
|
||||
.DS_Store
|
||||
tags
|
||||
|
|
6
COPYING
|
@ -1,7 +1,5 @@
|
|||
Yo, Sup Nostr Client
|
||||
Copyright (C) 2022 - 2023
|
||||
William Casarin <jb55@jb55.com>,
|
||||
Thomas Mathews <thomas.c.mathews@gmail.com>
|
||||
damus.io: damus web client and other things
|
||||
Copyright (C) 2022 William Casarin <jb55@jb55.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
|
|
11
Makefile
|
@ -1,11 +1,4 @@
|
|||
|
||||
all: fake
|
||||
@echo "you don't need to build anything."
|
||||
|
||||
tags: fake
|
||||
find js -name '*.js' | xargs ctags > "$@"
|
||||
|
||||
emojiregex: fake
|
||||
@curl -sL 'https://raw.githubusercontent.com/mathiasbynens/emoji-test-regex-pattern/main/dist/latest/javascript.txt'
|
||||
dist: fake
|
||||
rsync -avzP --exclude .git/ ./ charon:/www/damus.io/
|
||||
|
||||
.PHONY: fake
|
||||
|
|
28
README.md
|
@ -1,15 +1,23 @@
|
|||
# Yo Sup
|
||||
|
||||
[Issue Tracker](https://todo.sr.ht/~tomtom/damus-web-issues)
|
||||
# damus.io
|
||||
|
||||
"Yo Sup" is a minimal Nostr client that grew out of the original Damus Web
|
||||
code. It's goal is to view your feed and access your direct messages very fast.
|
||||
So fast it works over 3G with a fresh page load. It has no goals to fulfill any
|
||||
other NIPs, please use other clients such as Snort, Coracle, or Iris.
|
||||
The damus.io website
|
||||
|
||||
It's written in plain JavaScript, HTML, and CSS for ease of development and
|
||||
building. Small features and optimizations will be added as needed, but the
|
||||
application is considered "complete".
|
||||
## Damus Web
|
||||
|
||||
Patches are welcomed via email.
|
||||
You can find the damus web app in the `web` directory
|
||||
|
||||
## Contributing
|
||||
|
||||
You can send me patches over nostr or [email][email] at jb55@jb55.com
|
||||
|
||||
You can also just hit me up with a git-request-pull and ask me to pull one of
|
||||
your branches. eg, from github:
|
||||
|
||||
git request-pull origin/master https://github.com/bob/my-damus-io-fork
|
||||
|
||||
If you email or nostr me the output of this command I will be able to review &
|
||||
merge your changes!
|
||||
|
||||
[email]: https://git-send-email.io/
|
||||
|
||||
|
|
1
TODO
Normal file
|
@ -0,0 +1 @@
|
|||
video player needs to be outside the timeline, since re-rendering destroys it
|
56
android/index.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>damus</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="Damus">
|
||||
<meta property="og:description" content="A new social network that you control">
|
||||
<meta property="og:image" content="https://damus.io/img/damus.png">
|
||||
<meta property="og:url" content="https://damus.io">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<link rel="stylesheet" href="/css/normalize.css">
|
||||
<link rel="stylesheet" href="/css/skeleton.css?v=3">
|
||||
<link rel="stylesheet" href="/css/custom.css?v=4">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
<span class="damus">damus</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<section class="hero">
|
||||
<h2>Damus Android crowdfund</h2>
|
||||
<h5 class="subtitle">If ya'll help crowdfund me an android phone I can start working on an android version</h5>
|
||||
|
||||
<!-- <div class="row"> -->
|
||||
<!-- <h4 class="subtitle">Developers First</h4> -->
|
||||
<!-- <img class="code-example" src="img/code-placeholder.svg"> -->
|
||||
<!-- </div> -->
|
||||
</section>
|
||||
|
||||
<h4>Donations</h4>
|
||||
|
||||
<p>This is a bolt12 offer, you can pay this with a CLN node. Otherwise press the button to get a bolt11 invoice.</p>
|
||||
<a id="bolt12" href="lightning:lno1pgt5gctdw4ejqstwv3ex76tyyp3hymmhv3n82mnyzsyxgctdw4eju6t0rcs08sggen2ndwzjdpqlpfw9sgfth8n9sjs7kjfssrnurnp5lqk66ug">
|
||||
<div id="qrcode" class="tipjar-copy" style="float: left; width: 256px; cursor: pointer" ></div>
|
||||
</a>
|
||||
|
||||
<button style="margin-left: 20px; height: 50px" onclick="click_make_invoice(this)">Request bolt11 invoice</button>
|
||||
|
||||
<div id="tipjar-summary" style="clear: left; width: 100%; margin-bottom: 30px;">
|
||||
Loading donations...
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<script src="/js/qrcode.min.js" > </script>
|
||||
<script src="/js/lnsocket.js?v=10" > </script>
|
||||
<script src="/js/tipjar.js?v=23" ></script>
|
||||
</html>
|
5
channels/Makefile
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
index.html: channels.ejs ../stats/channels-last-week.json
|
||||
npx ejs $< -f ../stats/channels-last-week.json -o $@
|
||||
|
||||
|
69
channels/channels.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 50px 0 0 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 0;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.7em;
|
||||
margin-left: 10px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
padding-right: 18px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: -system-ui, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body {
|
||||
color: white;
|
||||
min-height: 800px;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
font-size: 20px;
|
||||
font-family: sans-serif;
|
||||
|
||||
background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, rgba(255,11,214,1) 100%);
|
||||
}
|
||||
|
||||
.channel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channel img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto 0 auto;
|
||||
max-width: 50em;
|
||||
hyphens: auto;
|
||||
word-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
61
channels/channels.ejs
Normal file
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Active nostr channels past week</title>
|
||||
<link rel="stylesheet" href="../log/comments.css?v=5">
|
||||
<link rel="stylesheet" href="channels.css?v=7">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
|
||||
<h2>Active nostr channels past week</h2>
|
||||
<table>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Messages</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% channels.forEach((channel) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="channel">
|
||||
<img onerror="this.onerror=null;this.src='https://robohash.org/<%= channel[3] %>'" src="<%= channel[2] %>" />
|
||||
<%= channel[1].slice(0,20) %>
|
||||
</div>
|
||||
</td>
|
||||
<td><%= channel[0] %></td>
|
||||
<td><pre><%= channel[3] %></pre></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Raw Data</h3>
|
||||
<a href="https://damus.io/stats/channels-last-week.json">json</a><br/>
|
||||
<a href="https://damus.io/stats/channels-last-week.csv">csv</a>
|
||||
|
||||
<!--
|
||||
<h3><a href="nostr:e:2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423">Comments</a></h3>
|
||||
<div id="comments">
|
||||
</div>
|
||||
<script src="nostr.js?v=4" ></script>
|
||||
<script src="comments.js?v=16" ></script>
|
||||
<script>
|
||||
const relay = comments_init("2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423")
|
||||
</script>
|
||||
-->
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
130
channels/package-lock.json
generated
Normal file
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"name": "stats",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz",
|
||||
"integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==",
|
||||
"requires": {
|
||||
"jake": "^10.8.5"
|
||||
}
|
||||
},
|
||||
"filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"requires": {
|
||||
"minimatch": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"jake": {
|
||||
"version": "10.8.5",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz",
|
||||
"integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==",
|
||||
"requires": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"filelist": "^1.0.1",
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
channels/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "stats",
|
||||
"version": "1.0.0",
|
||||
"description": "damus stats",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "jb55",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ejs": "^3.1.8"
|
||||
}
|
||||
}
|
191
css/custom.css
Normal file
|
@ -0,0 +1,191 @@
|
|||
@import url('https://rsms.me/inter/inter.css');
|
||||
html { font-family: 'Inter', sans-serif; }
|
||||
@supports (font-variation-settings: normal) {
|
||||
html { font-family: 'Inter var', sans-serif; }
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.blog-container {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
font-family: -system-ui, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote:before {
|
||||
color: #ccc;
|
||||
content: open-quote;
|
||||
display: block;
|
||||
position: relative;
|
||||
left: -0.6em;
|
||||
top: 0.3em;
|
||||
font-size: 4em;
|
||||
line-height: 0.1em;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
|
||||
blockquote:after {
|
||||
color: #ccc;
|
||||
content: close-quote;
|
||||
font-size: 4em;
|
||||
position:relative;
|
||||
top: 0.2em;
|
||||
left: -0.1em;
|
||||
line-height: 0.1em;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
label input {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
ul.socials {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.socials-container {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
justify-content: space-evenly;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
ul.socials > li {
|
||||
float: left;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.socials-container img {
|
||||
color: #ff0000;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 8rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-top: 5rem;
|
||||
text-align: center;
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin-left: 50px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.portrait {
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0px 0px 10px #aaa;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-img {
|
||||
margin: 2.5rem auto 2.5rem auto;
|
||||
width: 100px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.credits {
|
||||
margin-top: 10rem;
|
||||
text-align: center;
|
||||
color: #252D3A;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.value-prop {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.value-props {
|
||||
margin-bottom: 7rem;
|
||||
}
|
||||
|
||||
.damus {
|
||||
font-size: 4rem;
|
||||
letter-spacing: -0.08em;
|
||||
font-weight: 100;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 8rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.055em;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
margin-bottom: 0;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
padding-right: 18px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.wizards {
|
||||
font-size: 1.5rem;
|
||||
top: -2em;
|
||||
left: 0.2em;
|
||||
position: relative;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
body {
|
||||
letter-spacing: -0.044em;
|
||||
margin: 3rem 0 10em 0;
|
||||
color: white;
|
||||
background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, rgba(255,11,214,1) 100%);
|
||||
}
|
||||
|
||||
input {
|
||||
color: #252D3A;
|
||||
}
|
||||
|
429
css/normalize.css
vendored
Normal file
|
@ -0,0 +1,429 @@
|
|||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
@media only screen and (max-width: 988px)
|
||||
{
|
||||
h1 {
|
||||
font-size: calc(100vw / 12.2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
@media (max-width: 840px){
|
||||
/* Utility */
|
||||
.vertical-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Application Framework */
|
||||
#view {
|
||||
flex: 1;
|
||||
width: initial;
|
||||
border-right: none;
|
||||
}
|
||||
.nav.mobile {
|
||||
display: flex;
|
||||
}
|
||||
#content header > label {
|
||||
padding: 12px;
|
||||
}
|
||||
.nav.mobile .new-note {
|
||||
position: fixed;
|
||||
height: initial;
|
||||
bottom: 88px;
|
||||
right: 20px;
|
||||
z-index: var(--zGlobal);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Event */
|
||||
.pfp { /* TODO sync up with userpic */
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
dialog:modal {
|
||||
width: initial;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
421
css/skeleton.css
vendored
Normal file
|
@ -0,0 +1,421 @@
|
|||
/*
|
||||
* Skeleton V2.0.4
|
||||
* Copyright 2014, Dave Gamache
|
||||
* www.getskeleton.com
|
||||
* Free to use under the MIT license.
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* 12/29/2014
|
||||
*/
|
||||
|
||||
|
||||
/* Table of contents
|
||||
––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
- Grid
|
||||
- Base Styles
|
||||
- Typography
|
||||
- Links
|
||||
- Buttons
|
||||
- Forms
|
||||
- Lists
|
||||
- Code
|
||||
- Tables
|
||||
- Spacing
|
||||
- Utilities
|
||||
- Clearing
|
||||
- Media Queries
|
||||
*/
|
||||
|
||||
|
||||
/* Grid
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box; }
|
||||
.column,
|
||||
.columns {
|
||||
width: 100%;
|
||||
float: left;
|
||||
box-sizing: border-box; }
|
||||
|
||||
/* For devices larger than 400px */
|
||||
@media (min-width: 400px) {
|
||||
.container {
|
||||
width: 85%;
|
||||
padding: 0; }
|
||||
}
|
||||
|
||||
/* For devices larger than 550px */
|
||||
@media (min-width: 550px) {
|
||||
.container {
|
||||
width: 80%; }
|
||||
.column,
|
||||
.columns {
|
||||
margin-left: 4%; }
|
||||
.column:first-child,
|
||||
.columns:first-child {
|
||||
margin-left: 0; }
|
||||
|
||||
.one.column,
|
||||
.one.columns { width: 4.66666666667%; }
|
||||
.two.columns { width: 13.3333333333%; }
|
||||
.three.columns { width: 22%; }
|
||||
.four.columns { width: 30.6666666667%; }
|
||||
.five.columns { width: 39.3333333333%; }
|
||||
.six.columns { width: 48%; }
|
||||
.seven.columns { width: 56.6666666667%; }
|
||||
.eight.columns { width: 65.3333333333%; }
|
||||
.nine.columns { width: 74.0%; }
|
||||
.ten.columns { width: 82.6666666667%; }
|
||||
.eleven.columns { width: 91.3333333333%; }
|
||||
.twelve.columns { width: 100%; margin-left: 0; }
|
||||
|
||||
.one-third.column { width: 30.6666666667%; }
|
||||
.two-thirds.column { width: 65.3333333333%; }
|
||||
|
||||
.one-half.column { width: 48%; }
|
||||
|
||||
/* Offsets */
|
||||
.offset-by-one.column,
|
||||
.offset-by-one.columns { margin-left: 8.66666666667%; }
|
||||
.offset-by-two.column,
|
||||
.offset-by-two.columns { margin-left: 17.3333333333%; }
|
||||
.offset-by-three.column,
|
||||
.offset-by-three.columns { margin-left: 26%; }
|
||||
.offset-by-four.column,
|
||||
.offset-by-four.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-five.column,
|
||||
.offset-by-five.columns { margin-left: 43.3333333333%; }
|
||||
.offset-by-six.column,
|
||||
.offset-by-six.columns { margin-left: 52%; }
|
||||
.offset-by-seven.column,
|
||||
.offset-by-seven.columns { margin-left: 60.6666666667%; }
|
||||
.offset-by-eight.column,
|
||||
.offset-by-eight.columns { margin-left: 69.3333333333%; }
|
||||
.offset-by-nine.column,
|
||||
.offset-by-nine.columns { margin-left: 78.0%; }
|
||||
.offset-by-ten.column,
|
||||
.offset-by-ten.columns { margin-left: 86.6666666667%; }
|
||||
.offset-by-eleven.column,
|
||||
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
|
||||
|
||||
.offset-by-one-third.column,
|
||||
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-two-thirds.column,
|
||||
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
|
||||
|
||||
.offset-by-one-half.column,
|
||||
.offset-by-one-half.columns { margin-left: 52%; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Base Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/* NOTE
|
||||
html is set to 62.5% so that all the REM measurements throughout Skeleton
|
||||
are based on 10px sizing. So basically 1.5rem = 15px :) */
|
||||
html {
|
||||
font-size: 62.5%; }
|
||||
body {
|
||||
min-height: 800px;
|
||||
font-size: 1.6em; /* currently ems cause chrome bug misinterpreting rems on body element */
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
color: #222; }
|
||||
|
||||
|
||||
/* Typography
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 300; }
|
||||
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
|
||||
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
|
||||
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
|
||||
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
|
||||
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.08rem; }
|
||||
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
|
||||
|
||||
/* Larger than phablet */
|
||||
@media (min-width: 550px) {
|
||||
h1 { font-size: 5.0rem; }
|
||||
h2 { font-size: 4.2rem; }
|
||||
h3 { font-size: 3.6rem; }
|
||||
h4 { font-size: 3.0rem; }
|
||||
h5 { font-size: 2.4rem; }
|
||||
h6 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0; }
|
||||
|
||||
|
||||
/* Links
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
a {
|
||||
color: #0595ad;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #9BBDF2;
|
||||
}
|
||||
|
||||
|
||||
/* Buttons
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.button,
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"] {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 30px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box; }
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:hover,
|
||||
input[type="reset"]:hover,
|
||||
input[type="button"]:hover,
|
||||
.button:focus,
|
||||
button:focus,
|
||||
input[type="submit"]:focus,
|
||||
input[type="reset"]:focus,
|
||||
input[type="button"]:focus {
|
||||
color: #999;
|
||||
border-color: #999;
|
||||
outline: 0; }
|
||||
.button.button-primary,
|
||||
button.button-primary,
|
||||
input[type="submit"].button-primary,
|
||||
input[type="reset"].button-primary,
|
||||
input[type="button"].button-primary {
|
||||
color: #FFF;
|
||||
background-color: #33C3F0;
|
||||
border-color: #33C3F0; }
|
||||
.button.button-primary:hover,
|
||||
button.button-primary:hover,
|
||||
input[type="submit"].button-primary:hover,
|
||||
input[type="reset"].button-primary:hover,
|
||||
input[type="button"].button-primary:hover,
|
||||
.button.button-primary:focus,
|
||||
button.button-primary:focus,
|
||||
input[type="submit"].button-primary:focus,
|
||||
input[type="reset"].button-primary:focus,
|
||||
input[type="button"].button-primary:focus {
|
||||
color: #FFF;
|
||||
background-color: #1EAEDB;
|
||||
border-color: #1EAEDB; }
|
||||
|
||||
|
||||
/* Forms
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
select {
|
||||
height: 38px;
|
||||
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
background-color: #fff;
|
||||
border: 1px solid #D1D1D1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box; }
|
||||
/* Removes awkward default styles on some inputs for iOS */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none; }
|
||||
textarea {
|
||||
min-height: 65px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px; }
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="url"]:focus,
|
||||
input[type="password"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border: 1px solid #33C3F0;
|
||||
outline: 0; }
|
||||
label,
|
||||
legend {
|
||||
/* display: block; */
|
||||
margin-bottom: .5rem;
|
||||
/* font-weight: 600; */ }
|
||||
fieldset {
|
||||
padding: 0;
|
||||
border-width: 0; }
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline; }
|
||||
label > .label-body {
|
||||
display: inline-block;
|
||||
margin-left: .5rem;
|
||||
font-weight: normal; }
|
||||
|
||||
|
||||
/* Lists
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
ul {
|
||||
list-style: circle inside; }
|
||||
ol {
|
||||
list-style: decimal inside; }
|
||||
ol, ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0; }
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%; }
|
||||
li {
|
||||
margin-bottom: 0.2rem; }
|
||||
|
||||
|
||||
/* Code
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px; }
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre; }
|
||||
|
||||
|
||||
/* Tables
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1; }
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0; }
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0; }
|
||||
|
||||
|
||||
/* Spacing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem; }
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem; }
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 2.5rem; }
|
||||
|
||||
|
||||
/* Utilities
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.u-full-width {
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-max-full-width {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-pull-right {
|
||||
float: right; }
|
||||
.u-pull-left {
|
||||
float: left; }
|
||||
|
||||
|
||||
/* Misc
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
hr {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3.5rem;
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1; }
|
||||
|
||||
|
||||
/* Clearing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* Self Clearing Goodness */
|
||||
.container:after,
|
||||
.row:after,
|
||||
.u-cf {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
|
||||
|
||||
/* Media Queries
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/*
|
||||
Note: The best way to structure the use of media queries is to create the queries
|
||||
near the relevant code. For example, if you wanted to change the styles for buttons
|
||||
on small devices, paste the mobile query code up in the buttons section and style it
|
||||
there.
|
||||
*/
|
||||
|
||||
|
||||
/* Larger than mobile */
|
||||
@media (min-width: 400px) {}
|
||||
|
||||
/* Larger than phablet (also point when grid becomes active) */
|
||||
@media (min-width: 550px) {}
|
||||
|
||||
/* Larger than tablet */
|
||||
@media (min-width: 750px) {}
|
||||
|
||||
/* Larger than desktop */
|
||||
@media (min-width: 1000px) {}
|
||||
|
||||
/* Larger than Desktop HD */
|
||||
@media (min-width: 1200px) {}
|
645
css/styles.css
|
@ -1,645 +0,0 @@
|
|||
*:focus-visible {
|
||||
/* Technically this is bad and something else should be done to indicate
|
||||
* that something is in focus.
|
||||
*/
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--clrBg);
|
||||
color: var(--clrText);
|
||||
font-family: "Noto Sans", sans-serif;
|
||||
font-size: var(--fsNormal);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--clrLink);
|
||||
}
|
||||
a:visited {
|
||||
color: var(--clrLinkVisited);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead {
|
||||
font-weight: bold;
|
||||
}
|
||||
th, td {
|
||||
padding: 5px 0;
|
||||
font-size: var(--fsNormal);
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.row {
|
||||
margin: 15px 0;
|
||||
}
|
||||
.mr-some {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Welcome */
|
||||
|
||||
#container-busy .loader {
|
||||
height: 100vh;
|
||||
}
|
||||
.page-content {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.hero-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.hero-box > .padded {
|
||||
/* TODO rename .padded */
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.btn-text {
|
||||
border: none;
|
||||
font-size: var(--fsNormal);
|
||||
color: var(--clrText);
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
#view footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: var(--zHeader);
|
||||
}
|
||||
.nav.full {
|
||||
border-right: 1px solid var(--clrBorder);
|
||||
padding: 10px;
|
||||
}
|
||||
.nav.full > div {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
.nav.full > div > * {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.nav.mobile {
|
||||
display: none;
|
||||
background: var(--clrBg);
|
||||
flex-direction: row;
|
||||
border-top: var(--clrBorder) 1px solid;
|
||||
}
|
||||
.nav.mobile button {
|
||||
padding: 18px;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
.nav [data-view].active img.inactive,
|
||||
.nav [data-view] img.active {
|
||||
display: none;
|
||||
}
|
||||
.nav [data-view].active img.active {
|
||||
display: block;
|
||||
}
|
||||
button.nav > img.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.nav button.new-note {
|
||||
background: var(--clrBgAction);
|
||||
height: 56px;
|
||||
border-radius: 38px;
|
||||
}
|
||||
#app-icon-logo > img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.new-notifications {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 13px;
|
||||
border-radius: 13px;
|
||||
background: var(--clrNotification);
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* Application Framework */
|
||||
#container {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
#view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--clrBorder);
|
||||
width: 750px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
#view > header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: var(--zHeader);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
#view > header > label {
|
||||
padding: 15px;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
display: block;
|
||||
}
|
||||
#timeline, #settings, #dms {
|
||||
flex: 1;
|
||||
}
|
||||
#header-tools {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
#header-tools > * {
|
||||
margin-left: 15px;
|
||||
}
|
||||
#header-tools .pfp {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Events & Content */
|
||||
.events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.events.reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.event {
|
||||
padding: 15px;
|
||||
transition: background-color 0.2s linear;
|
||||
}
|
||||
.event:hover {
|
||||
background-color: var(--clrPanel);
|
||||
}
|
||||
.loading-events {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
}
|
||||
.loader {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
.loader img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.show-new, .show-more {
|
||||
text-align: center;
|
||||
}
|
||||
.show-more > button,
|
||||
.show-new > button {
|
||||
color: var(--clrText);
|
||||
border: none;
|
||||
padding: 15px;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font-size: var(--fsNormal);
|
||||
font-weight: bold;
|
||||
}
|
||||
.userpic { /* TODO remove .userpic and use helper class */
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.pfp {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
z-index: var(--zPFP);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.event-content {
|
||||
flex: 1;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.event-content > .info {
|
||||
display: inline-block;
|
||||
}
|
||||
.username, .thread-id {
|
||||
font-weight: 800;
|
||||
font-size: var(--fsReduced);
|
||||
word-break: break-word;
|
||||
}
|
||||
.chatroom-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.deleted-comment {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.line-bot {
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
top: -7px;
|
||||
left: calc(50% - 1px);
|
||||
background-color: var(--clrBorder);
|
||||
}
|
||||
.quote {
|
||||
margin-left: 10px;
|
||||
padding: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#replying-to {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#replybox {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--clrBorder);
|
||||
}
|
||||
|
||||
.shared-by {
|
||||
font-size: var(--fsReduced);
|
||||
color: var(--clrTextLight);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.timestamp, .replying-to {
|
||||
font-size: var(--fsSmall);
|
||||
color: var(--clrTextLight);
|
||||
}
|
||||
.comment {
|
||||
word-break: break-word;
|
||||
}
|
||||
.inline-img {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.action-bar {
|
||||
display: flex;
|
||||
}
|
||||
.action-bar > button {
|
||||
opacity: 0.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reactions {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.reaction-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 2px solid var(--clrBorder);
|
||||
padding: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.reaction-group > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
.reaction-group img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
margin-left: -8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.reaction-group img:first-of-type {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.reaction-emoji {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.action-bar button.icon {
|
||||
transition: opacity 0.3s linear;
|
||||
}
|
||||
.action-bar button.icon img.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.action-bar button.icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-bar button.heart.liked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
details.cw summary {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
dialog:modal {
|
||||
width: 80%;
|
||||
max-width: 700px;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--clrText);
|
||||
}
|
||||
dialog::backdrop {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
dialog header {
|
||||
display: flex;
|
||||
}
|
||||
dialog header label {
|
||||
flex: 1;
|
||||
font-weight: 800;
|
||||
font-size: var(--fsEnlarged);
|
||||
word-break: break-word;
|
||||
}
|
||||
dialog header button {
|
||||
font-size: 24px;
|
||||
}
|
||||
#media-preview {
|
||||
height: 100%;
|
||||
max-width: inherit;
|
||||
}
|
||||
#media-preview > img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
dialog > .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--clrPanel);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
dialog > .container .max-content {
|
||||
max-height: min(100vh/2, 500px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Post & Reply */
|
||||
#newpost {
|
||||
padding: 0px 15px 15px;
|
||||
border-bottom: solid 1px var(--clrBorder);
|
||||
}
|
||||
textarea.post-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 25px;
|
||||
}
|
||||
textarea.post-input.dm {
|
||||
background: var(--clrBorder);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
min-height: 45px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.post-tools {
|
||||
text-align: right;
|
||||
}
|
||||
.post-tools > button.icon {
|
||||
margin-right: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.post-tools > button.icon.cw.active {
|
||||
opacity: 1.0;
|
||||
}
|
||||
.post-tools > button[name='reply-all'] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
input[type="text"].cw {
|
||||
border: none;
|
||||
border-bottom: solid 2px var(--clrWarn);
|
||||
font-size: var(--fsReduced);
|
||||
background: transparent;
|
||||
color: var(--clrText);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Profile */
|
||||
|
||||
.pfp.jumbo {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
#profile-info > .profile-banner {
|
||||
display: block;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #1a1a1a;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
#profile-info > div {
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
}
|
||||
#profile-info > div:last-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
.profile-tools {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
.profile-tools > button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.profile-tools > button.icon {
|
||||
margin-right: 20px;
|
||||
}
|
||||
p[name="profile-about"] {
|
||||
margin: 0;
|
||||
}
|
||||
label[name="profile-nip05"] {
|
||||
font-weight: 800;
|
||||
display: block;
|
||||
font-size: var(--fsEnlarged);
|
||||
}
|
||||
|
||||
/* Profile Editor */
|
||||
|
||||
#profile-editor header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#profile-editor textarea {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
/* Event Preview */
|
||||
|
||||
#event-details .modal-content > div {
|
||||
overflow: scroll;
|
||||
}
|
||||
code {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
|
||||
#settings section {
|
||||
margin: 15px;
|
||||
}
|
||||
#settings header > label {
|
||||
font-weight: bold;
|
||||
font-size: var(--fsLarge);
|
||||
}
|
||||
|
||||
/* Messaging */
|
||||
|
||||
#dm-post {
|
||||
padding: 15px;
|
||||
background: var(--clrBg);
|
||||
border-top: 1px solid var(--clrBorder);
|
||||
}
|
||||
.dm-group {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
}
|
||||
.dm-group .content {
|
||||
position: relative;
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
.dm-group .message {
|
||||
word-break: break-word;
|
||||
}
|
||||
.dm-group .time {
|
||||
font-size: var(--fsReduced);
|
||||
color: var(--clrTextLight);
|
||||
}
|
||||
.dm-group .count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--clrBorder);
|
||||
border-radius: 20px;
|
||||
padding: 1px 8px;
|
||||
font-size: var(--fsSmall);
|
||||
}
|
||||
.dm-group .count.active {
|
||||
background: var(--clrChatBlue);
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
.event.dm {
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
}
|
||||
.event.dm:hover {
|
||||
background: transparent;
|
||||
}
|
||||
.event.dm:last-child {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.event.dm .wrap{
|
||||
border-radius: 20px;
|
||||
background: var(--clrPanel);
|
||||
padding: 10px;
|
||||
}
|
||||
.event.dm.mine .wrap {
|
||||
color: white;
|
||||
background: var(--clrChatBlue);
|
||||
margin-left: auto;
|
||||
}
|
||||
.event.dm.mine .timestamp {
|
||||
color: white;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
.event.dm .body p {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .timestamp {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .reactions {
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Prevent events from inside button sub elements */
|
||||
button > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--clrText);
|
||||
font-size: var(--fsNormal);
|
||||
padding: 15px;
|
||||
border-bottom: 3px var(--clrText) solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--clrText);
|
||||
font-size: var(--fsEnlarged);
|
||||
font-family: var(--ffDefault);
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.icon.svg {
|
||||
filter: invert(1);
|
||||
}
|
||||
.icon.svg.dark-noinvert {
|
||||
filter: invert(0);
|
||||
}
|
||||
.modal {
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
||||
}
|
||||
|
BIN
favicon.ico
Normal file
After Width: | Height: | Size: 17 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 368C269.3 368 280 357.3 280 344V280H344C357.3 280 368 269.3 368 256C368 242.7 357.3 232 344 232H280V168C280 154.7 269.3 144 256 144C242.7 144 232 154.7 232 168V232H168C154.7 232 144 242.7 144 256C144 269.3 154.7 280 168 280H232V344C232 357.3 242.7 368 256 368z"/></svg>
|
Before Width: | Height: | Size: 620 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M223.1 256c70.7 0 128-57.31 128-128s-57.3-128-128-128C153.3 0 96 57.31 96 128S153.3 256 223.1 256zM274.7 304H173.3C77.61 304 0 381.7 0 477.4C0 496.5 15.52 512 34.66 512h286.4c-1.246-5.531-1.43-11.31-.2832-17.04l14.28-71.41c1.943-9.723 6.676-18.56 13.68-25.56l45.72-45.72C363.3 322.4 321.2 304 274.7 304zM371.4 420.6c-2.514 2.512-4.227 5.715-4.924 9.203l-14.28 71.41c-1.258 6.289 4.293 11.84 10.59 10.59l71.42-14.29c3.482-.6992 6.682-2.406 9.195-4.922l125.3-125.3l-72.01-72.01L371.4 420.6zM629.5 255.7l-21.1-21.11c-14.06-14.06-36.85-14.06-50.91 0l-38.13 38.14l72.01 72.01l38.13-38.13C643.5 292.5 643.5 269.7 629.5 255.7z"/></svg>
|
Before Width: | Height: | Size: 867 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M424 80C437.3 80 448 90.75 448 104C448 117.3 437.3 128 424 128H412.4L388.4 452.7C385.9 486.1 358.1 512 324.6 512H123.4C89.92 512 62.09 486.1 59.61 452.7L35.56 128H24C10.75 128 0 117.3 0 104C0 90.75 10.75 80 24 80H93.82L130.5 24.94C140.9 9.357 158.4 0 177.1 0H270.9C289.6 0 307.1 9.358 317.5 24.94L354.2 80H424zM177.1 48C174.5 48 171.1 49.34 170.5 51.56L151.5 80H296.5L277.5 51.56C276 49.34 273.5 48 270.9 48H177.1zM364.3 128H83.69L107.5 449.2C108.1 457.5 115.1 464 123.4 464H324.6C332.9 464 339.9 457.5 340.5 449.2L364.3 128z"/></svg>
|
Before Width: | Height: | Size: 773 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M414.9 31.11L270.9 495.1C266.1 507.8 253.5 514.8 240.9 510.9C228.2 506.1 221.1 493.5 225.1 480.9L369.1 16.89C373 4.226 386.5-2.852 399.1 1.077C411.8 5.006 418.9 18.45 414.9 31.11V31.11zM504.4 118.5L632.4 238.5C637.3 243 640 249.4 640 255.1C640 262.6 637.3 268.1 632.4 273.5L504.4 393.5C494.7 402.6 479.6 402.1 470.5 392.4C461.4 382.7 461.9 367.6 471.6 358.5L580.9 255.1L471.6 153.5C461.9 144.4 461.4 129.3 470.5 119.6C479.6 109.9 494.7 109.4 504.4 118.5V118.5zM168.4 153.5L59.09 255.1L168.4 358.5C178.1 367.6 178.6 382.7 169.5 392.4C160.4 402.1 145.3 402.6 135.6 393.5L7.585 273.5C2.746 268.1 0 262.6 0 255.1C0 249.4 2.746 243 7.585 238.5L135.6 118.5C145.3 109.4 160.4 109.9 169.5 119.6C178.6 129.3 178.1 144.4 168.4 153.5V153.5z"/></svg>
|
Before Width: | Height: | Size: 977 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M244 84L255.1 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 0 232.4 0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84C243.1 84 244 84.01 244 84L244 84zM255.1 163.9L210.1 117.1C188.4 96.28 157.6 86.4 127.3 91.44C81.55 99.07 48 138.7 48 185.1V190.9C48 219.1 59.71 246.1 80.34 265.3L256 429.3L431.7 265.3C452.3 246.1 464 219.1 464 190.9V185.1C464 138.7 430.4 99.07 384.7 91.44C354.4 86.4 323.6 96.28 301.9 117.1L255.1 163.9z"/></svg>
|
Before Width: | Height: | Size: 869 B |
|
@ -1,6 +0,0 @@
|
|||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||
<path
|
||||
d="m 0,190.9 v -5.8 c 0,-69.9 50.52,-129.52 119.4,-141 44.7,-7.59 92,7.27 124.6,39.92 L 256,96 267.1,84.02 C 300.6,51.37 347,36.51 392.6,44.1 461.5,55.58 512,115.2 512,185.1 v 5.8 c 0,41.5 -17.2,81.2 -47.6,109.5 L 283.7,469.1 c -7.5,7 -17.4,10.9 -27.7,10.9 -10.3,0 -20.2,-3.9 -27.7,-10.9 L 47.59,300.4 C 17.23,272.1 3e-4,232.4 3e-4,190.9 Z"
|
||||
style="fill:#ff5757;fill-opacity:1" />
|
||||
</svg>
|
Before Width: | Height: | Size: 639 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M64 368C90.51 368 112 389.5 112 416C112 442.5 90.51 464 64 464C37.49 464 16 442.5 16 416C16 389.5 37.49 368 64 368zM64 208C90.51 208 112 229.5 112 256C112 282.5 90.51 304 64 304C37.49 304 16 282.5 16 256C16 229.5 37.49 208 64 208zM64 144C37.49 144 16 122.5 16 96C16 69.49 37.49 48 64 48C90.51 48 112 69.49 112 96C112 122.5 90.51 144 64 144z"/></svg>
|
Before Width: | Height: | Size: 588 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M136.3 226.2l176 151.1c15.38 13.3 39.69 2.545 39.69-18.16V275.1c108.5 12.58 151.1 58.79 112.6 181.9c-5.031 16.09 14.41 28.56 28.06 18.62c43.75-31.81 83.34-92.69 83.34-154.1c0-131.3-94.86-173.2-224-183.5V56.02c0-20.67-24.28-31.46-39.69-18.16L136.3 189.9C125.2 199.4 125.2 216.6 136.3 226.2zM8.31 226.2l176 151.1c15.38 13.3 39.69 2.545 39.69-18.16v-15.83L66.33 208l157.7-136.2V56.02c0-20.67-24.28-31.46-39.69-18.16l-176 151.1C-2.77 199.4-2.77 216.6 8.31 226.2z"/></svg>
|
Before Width: | Height: | Size: 706 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M614.2 334.8C610.5 325.8 601.7 319.1 592 319.1H544V176C544 131.9 508.1 96 464 96h-128c-17.67 0-32 14.31-32 32s14.33 32 32 32h128C472.8 160 480 167.2 480 176v143.1h-48c-9.703 0-18.45 5.844-22.17 14.82s-1.656 19.29 5.203 26.16l80 80.02C499.7 445.7 505.9 448 512 448s12.28-2.344 16.97-7.031l80-80.02C615.8 354.1 617.9 343.8 614.2 334.8zM304 352h-128C167.2 352 160 344.8 160 336V192h48c9.703 0 18.45-5.844 22.17-14.82s1.656-19.29-5.203-26.16l-80-80.02C140.3 66.34 134.1 64 128 64S115.7 66.34 111 71.03l-80 80.02C24.17 157.9 22.11 168.2 25.83 177.2S38.3 192 48 192H96V336C96 380.1 131.9 416 176 416h128c17.67 0 32-14.31 32-32S321.7 352 304 352z"/></svg>
|
Before Width: | Height: | Size: 887 B |
|
@ -1,4 +0,0 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="#0F0F0F"/>
|
||||
<path d="M365.786 182.023C359.393 182.023 353.27 183.184 347.603 185.306C337.452 175.718 325.012 169.721 310.5 169.721C301.562 169.721 293.269 172.199 285.906 176.193L294.444 123.182C294.922 120.204 295.154 117.243 295.154 114.318C295.154 86.5781 272.955 59 239.799 59C213.17 59 189.65 78.3439 185.258 105.545L182.575 122.223L179.12 114.957C169.598 95.1598 149.887 83.6042 129.293 83.6042C97.9648 83.6042 74 109.191 74 139.014C74 147.071 75.763 155.244 79.4856 162.986L117.806 242.684C99.0629 251.525 86.2857 270.977 86.2857 292.043V353.549C86.2857 370.51 94.2714 386.855 107.655 397.265L142.938 424.743C165.475 442.296 193.601 451.968 222.181 451.968L282.696 451.995C357.262 452.652 418 391.222 418 317.184V234.534C418 204.78 393.352 182.023 365.786 182.023ZM295.143 222.233C295.143 213.622 301.363 206.625 310.5 206.625C318.962 206.625 325.857 213.605 325.857 222.194V264.864C325.857 273.437 318.946 280.433 310.5 280.433C301.363 280.433 295.143 273.437 295.143 264.903V222.233ZM239.78 95.8979C249.033 95.8979 258.309 103.21 258.309 114.334C258.309 115.316 258.23 116.31 258.068 117.311L240.164 228.537C234.252 226.307 227.879 224.385 221.352 224.385C217.916 224.385 214.466 224.708 211.078 225.349L202.977 226.6L221.651 110.737C223.118 102.387 230.95 95.8979 239.78 95.8979ZM110.166 138.968C110.166 128.765 118.451 120.485 128.61 120.485C135.472 120.485 142.047 124.347 145.203 130.949L192.265 228.821L154.855 234.688L112.685 146.964C111.441 144.427 110.166 141.659 110.166 138.968ZM282.704 415.748L222.189 415.721C201.794 415.721 181.707 408.814 165.605 396.269L130.307 368.784C125.807 365.313 123.143 359.854 123.143 353.549V292.043C123.143 283.409 129.262 275.813 136.649 274.19L217.505 261.712C218.818 262.135 220.123 261.289 221.352 261.289C230.497 261.289 239.849 268.609 239.849 279.787C239.849 288.275 234.081 295.895 226.665 297.54L182.114 304.267C172.877 305.523 166.158 313.432 166.158 322.527C166.158 338.065 180.609 341.04 184.356 341.04C186.387 341.04 233.968 334.414 234.682 334.255C252.765 330.236 266.556 317.164 272.867 300.703C282.32 310.802 295.604 317.337 310.5 317.337C316.893 317.337 323.024 316.176 328.691 314.054C338.143 323.642 351.273 329.639 365.786 329.639C365.939 329.639 373.526 329.296 380.106 327.394C374.923 376.922 333.459 415.748 282.704 415.748ZM381.143 277.204C381.143 285.738 374.232 292.735 365.786 292.735C357.324 292.735 350.429 285.755 350.429 277.166V234.495C350.429 225.923 356.648 218.927 365.786 218.927C374.247 218.927 381.143 225.906 381.143 234.495V277.204Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,4 +0,0 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M402 159.961C393.674 159.961 385.7 161.47 378.32 164.229C365.1 151.763 348.9 143.966 330 143.966C318.36 143.966 307.56 147.188 297.97 152.381L309.09 83.4532C309.713 79.5805 310.014 75.7307 310.014 71.9269C310.014 35.8585 281.104 0 237.924 0C203.244 0 172.614 25.1519 166.894 60.5206L163.4 82.2056L158.9 72.7586C146.5 47.0169 120.83 31.9917 94.01 31.9917C53.21 31.9917 22 65.2611 22 104.039C22 114.515 24.296 125.142 29.144 135.209L79.05 238.835C54.64 250.332 38 275.624 38 303.015V382.989C38 405.042 48.4 426.295 65.83 439.831L111.78 475.559C141.13 498.382 177.76 510.958 214.98 510.958L293.79 510.993C390.9 511.848 470 431.973 470 335.704V228.239C470 189.551 437.9 159.961 402 159.961Z" fill="#0D0D0D"/>
|
||||
<path d="M402 159.961C393.674 159.961 385.7 161.47 378.32 164.229C365.1 151.763 348.9 143.966 330 143.966C318.36 143.966 307.56 147.188 297.97 152.381L309.09 83.4532C309.713 79.5805 310.014 75.7307 310.014 71.9269C310.014 35.8585 281.104 0 237.924 0C203.244 0 172.614 25.1519 166.894 60.5206L163.4 82.2056L158.9 72.7586C146.5 47.0169 120.83 31.9917 94.01 31.9917C53.21 31.9917 22 65.2611 22 104.039C22 114.515 24.296 125.142 29.144 135.209L79.05 238.835C54.64 250.332 38 275.624 38 303.015V382.989C38 405.042 48.4 426.295 65.83 439.831L111.78 475.559C141.13 498.382 177.76 510.958 214.98 510.958L293.79 510.993C390.9 511.848 470 431.973 470 335.704V228.239C470 189.551 437.9 159.961 402 159.961ZM310 212.244C310 201.047 318.1 191.95 330 191.95C341.02 191.95 350 201.025 350 212.194V267.676C350 278.823 341 287.92 330 287.92C318.1 287.92 310 278.823 310 267.726V212.244ZM237.9 47.9766C249.95 47.9766 262.03 57.4846 262.03 71.9489C262.03 73.2255 261.928 74.5181 261.717 75.8187L238.4 220.441C230.7 217.542 222.4 215.043 213.9 215.043C209.425 215.043 204.933 215.463 200.52 216.297L189.97 217.923L214.29 67.2714C216.2 56.4139 226.4 47.9766 237.9 47.9766ZM69.1 103.979C69.1 90.7129 79.89 79.9463 93.12 79.9463C102.057 79.9463 110.62 84.9677 114.73 93.552L176.02 220.811L127.3 228.439L72.38 114.375C70.76 111.076 69.1 107.478 69.1 103.979ZM293.8 463.863L214.99 463.828C188.43 463.828 162.27 454.847 141.3 438.536L95.33 402.798C89.47 398.284 86 391.186 86 382.989V303.015C86 291.788 93.969 281.912 103.59 279.802L208.89 263.577C210.6 264.127 212.3 263.028 213.9 263.028C225.81 263.028 237.99 272.546 237.99 287.08C237.99 298.116 230.477 308.023 220.82 310.162L162.8 318.91C150.77 320.542 142.02 330.826 142.02 342.652C142.02 362.856 160.84 366.724 165.72 366.724C168.365 366.724 230.33 358.108 231.26 357.901C254.81 352.676 272.77 335.678 280.99 314.275C293.3 327.407 310.6 335.904 330 335.904C338.326 335.904 346.31 334.395 353.69 331.636C366 344.102 383.1 351.899 402 351.899C402.199 351.899 412.08 351.454 420.65 348.98C413.9 413.379 359.9 463.863 293.8 463.863ZM422 283.721C422 294.817 413 303.914 402 303.914C390.98 303.914 382 294.839 382 283.671V228.189C382 217.042 390.1 207.945 402 207.945C413.02 207.945 422 217.02 422 228.189V283.721Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M227.3 265.9L141.3 352L182.6 393.4C195.1 405.9 195.1 426.1 182.6 438.6C170.1 451.1 149.9 451.1 137.4 438.6L96 397.3L77.25 416L118.6 457.4C131.1 469.9 131.1 490.1 118.6 502.6C106.1 515.1 85.87 515.1 73.37 502.6L9.372 438.6C-3.124 426.1-3.124 405.9 9.372 393.4L182.1 220.7C168.1 198.5 159.1 172.2 159.1 144C159.1 64.47 224.5 0 304 0C383.5 0 448 64.47 448 144C448 223.5 383.5 288 304 288C275.8 288 249.5 279.9 227.3 265.9H227.3zM304 224C348.2 224 384 188.2 384 144C384 99.82 348.2 64 304 64C259.8 64 224 99.82 224 144C224 188.2 259.8 224 304 224z"/></svg>
|
Before Width: | Height: | Size: 791 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M256 352c-16.53 0-33.06-5.422-47.16-16.41L0 173.2V400C0 426.5 21.49 448 48 448h416c26.51 0 48-21.49 48-48V173.2l-208.8 162.5C289.1 346.6 272.5 352 256 352zM16.29 145.3l212.2 165.1c16.19 12.6 38.87 12.6 55.06 0l212.2-165.1C505.1 137.3 512 125 512 112C512 85.49 490.5 64 464 64h-416C21.49 64 0 85.49 0 112C0 125 6.01 137.3 16.29 145.3z"/></svg>
|
Before Width: | Height: | Size: 581 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M448 64H64C28.65 64 0 92.65 0 128v256c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V128C512 92.65 483.3 64 448 64zM64 112h384c8.822 0 16 7.178 16 16v22.16l-166.8 138.1c-23.19 19.28-59.34 19.27-82.47 .0156L48 150.2V128C48 119.2 55.18 112 64 112zM448 400H64c-8.822 0-16-7.178-16-16V212.7l136.1 113.4C204.3 342.8 229.8 352 256 352s51.75-9.188 71.97-25.98L464 212.7V384C464 392.8 456.8 400 448 400z"/></svg>
|
Before Width: | Height: | Size: 646 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C495.8 61.87 498.5 65.24 500.9 68.79C517.3 93.63 514.6 127.4 492.7 149.3L188.5 453.4C187.2 454.7 185.9 455.1 184.5 457.2C174.9 465.7 163.5 471.1 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9zM59.44 452.6L137.6 429.6C139.9 428.9 142.2 427.1 144.4 426.9L108.1 419.6C100.2 418 93.97 411.8 92.39 403.9L85.13 367.6C84.02 369.8 83.11 372.1 82.42 374.4L59.44 452.6zM180.7 393.3L383 191L320.1 128.1L118.7 331.3L128.1 383L180.7 393.3z"/></svg>
|
Before Width: | Height: | Size: 839 B |
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Written by Treer (gitlab.com/Treer) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="600"
|
||||
height="600"
|
||||
fill="white"
|
||||
id="svg562"
|
||||
sodipodi:docname="no-user.svg"
|
||||
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview564"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.7116667"
|
||||
inkscape:cx="248.00389"
|
||||
inkscape:cy="291.82084"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1388"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg562" />
|
||||
<title
|
||||
id="title549">Abstract user icon</title>
|
||||
<defs
|
||||
id="defs554">
|
||||
<clipPath
|
||||
id="circular-border">
|
||||
<circle
|
||||
cx="300"
|
||||
cy="300"
|
||||
r="250"
|
||||
id="circle551" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect
|
||||
style="fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:0"
|
||||
id="rect2537"
|
||||
width="600"
|
||||
height="600"
|
||||
x="4.9909852e-07"
|
||||
y="4.9906225e-07" />
|
||||
<circle
|
||||
cx="300"
|
||||
cy="230"
|
||||
r="100"
|
||||
id="circle558" />
|
||||
<circle
|
||||
cx="300"
|
||||
cy="550"
|
||||
clip-path="url(#circular-border)"
|
||||
id="circle560"
|
||||
r="190" />
|
||||
<metadata
|
||||
id="metadata4117">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Abstract user icon</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
Before Width: | Height: | Size: 2 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M72 48C85.25 48 96 58.75 96 72V120C96 133.3 85.25 144 72 144V232H128C128 218.7 138.7 208 152 208H200C213.3 208 224 218.7 224 232V280C224 293.3 213.3 304 200 304H152C138.7 304 128 293.3 128 280H72V384C72 388.4 75.58 392 80 392H128C128 378.7 138.7 368 152 368H200C213.3 368 224 378.7 224 392V440C224 453.3 213.3 464 200 464H152C138.7 464 128 453.3 128 440H80C49.07 440 24 414.9 24 384V144C10.75 144 0 133.3 0 120V72C0 58.75 10.75 48 24 48H72zM160 96C160 78.33 174.3 64 192 64H480C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H192C174.3 128 160 113.7 160 96zM288 256C288 238.3 302.3 224 320 224H480C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H320C302.3 288 288 273.7 288 256zM288 416C288 398.3 302.3 384 320 384H480C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H320C302.3 448 288 433.7 288 416z"/></svg>
|
Before Width: | Height: | Size: 1 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M112 96C112 122.5 90.51 144 64 144C37.49 144 16 122.5 16 96C16 69.49 37.49 48 64 48C90.51 48 112 69.49 112 96zM16 256C16 229.5 37.49 208 64 208C90.51 208 112 229.5 112 256C112 282.5 90.51 304 64 304C37.49 304 16 282.5 16 256zM16 416C16 389.5 37.49 368 64 368C90.51 368 112 389.5 112 416C112 442.5 90.51 464 64 464C37.49 464 16 442.5 16 416zM210.7 48H424C437.3 48 448 58.75 448 72V120C448 133.3 437.3 144 424 144H210.7C203.7 144 197 141.8 191.5 137.6L153.1 108.8C144.5 102.4 144.5 89.6 153.1 83.2L191.5 54.4C197 50.25 203.7 48 210.7 48V48zM153.1 243.2L191.5 214.4C197 210.2 203.7 208 210.7 208H488C501.3 208 512 218.7 512 232V280C512 293.3 501.3 304 488 304H210.7C203.7 304 197 301.8 191.5 297.6L153.1 268.8C144.5 262.4 144.5 249.6 153.1 243.2V243.2zM153.1 403.2L191.5 374.4C197 370.2 203.7 368 210.7 368H424C437.3 368 448 378.7 448 392V440C448 453.3 437.3 464 424 464H210.7C203.7 464 197 461.8 191.5 457.6L153.1 428.8C144.5 422.4 144.5 409.6 153.1 403.2z"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0zM256 464C263.4 464 282.1 456.8 303.6 415.6C312.4 397.9 319.1 376.4 325.6 352H186.4C192 376.4 199.6 397.9 208.4 415.6C229 456.8 248.6 464 256 464zM178.5 304H333.5C335.1 288.7 336 272.6 336 256C336 239.4 335.1 223.3 333.5 208H178.5C176.9 223.3 176 239.4 176 256C176 272.6 176.9 288.7 178.5 304V304zM325.6 160C319.1 135.6 312.4 114.1 303.6 96.45C282.1 55.22 263.4 48 256 48C248.6 48 229 55.22 208.4 96.45C199.6 114.1 192 135.6 186.4 160H325.6zM381.8 208C383.2 223.5 384 239.6 384 256C384 272.4 383.2 288.5 381.8 304H458.4C462.1 288.6 464 272.5 464 256C464 239.5 462.1 223.4 458.4 208H381.8zM342.1 66.61C356.2 92.26 367.4 124.1 374.7 160H440.6C419.2 118.9 384.4 85.88 342.1 66.61zM169.9 66.61C127.6 85.88 92.84 118.9 71.43 160H137.3C144.6 124.1 155.8 92.26 169.9 66.61V66.61zM48 256C48 272.5 49.93 288.6 53.57 304H130.2C128.8 288.5 128 272.4 128 256C128 239.6 128.8 223.5 130.2 208H53.57C49.93 223.4 48 239.5 48 256zM440.6 352H374.7C367.4 387.9 356.2 419.7 342.1 445.4C384.4 426.1 419.2 393.1 440.6 352zM137.3 352H71.43C92.84 393.1 127.6 426.1 169.9 445.4C155.8 419.7 144.6 387.9 137.3 352V352z"/></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M289.7 .0006C308.8 .0006 322.6 18.26 317.4 36.61L263.8 224H349.1C368.4 224 384 239.6 384 258.9C384 269.2 379.5 278.9 371.7 285.6L112.9 505.2C107.7 509.6 101.1 512 94.27 512C75.18 512 61.4 493.7 66.64 475.4L120.2 288H33.74C15.1 288 0 272.9 0 254.3C0 244.4 4.315 235 11.81 228.6L271.1 6.893C276.3 2.445 282.9 0 289.7 0V.0006zM253.6 84.99L72.36 240H152C159.5 240 166.6 243.5 171.2 249.5C175.7 255.6 177.1 263.4 175.1 270.6L130.3 427.5L313.5 272H232C224.5 272 217.4 268.5 212.8 262.5C208.3 256.4 206.9 248.6 208.9 241.4L253.6 84.99z"/></svg>
|
Before Width: | Height: | Size: 776 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 182.6l-45.25-45.25C451.4 131.4 443.3 128 434.8 128H384V80C384 53.5 362.5 32 336 32h-160C149.5 32 128 53.5 128 80V128H77.25c-8.5 0-16.62 3.375-22.62 9.375L9.375 182.6C3.375 188.6 0 196.8 0 205.3V304h128v-32C128 263.1 135.1 256 144 256h32C184.9 256 192 263.1 192 272v32h128v-32C320 263.1 327.1 256 336 256h32C376.9 256 384 263.1 384 272v32h128V205.3C512 196.8 508.6 188.6 502.6 182.6zM336 128h-160V80h160V128zM384 368c0 8.875-7.125 16-16 16h-32c-8.875 0-16-7.125-16-16v-32H192v32C192 376.9 184.9 384 176 384h-32C135.1 384 128 376.9 128 368v-32H0V448c0 17.62 14.38 32 32 32h448c17.62 0 32-14.38 32-32v-112h-128V368z"/></svg>
|
Before Width: | Height: | Size: 867 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 214.6l-77.25-77.25C419.4 131.4 411.2 128 402.7 128H384V88C384 57.13 358.9 32 328 32h-144C153.1 32 128 57.13 128 88V128H109.3C100.8 128 92.63 131.4 86.63 137.4L9.373 214.6C3.371 220.6 0 228.8 0 237.3V416c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V237.3C512 228.8 508.6 220.6 502.6 214.6zM176 88c0-4.406 3.594-8 8-8h144c4.406 0 8 3.594 8 8V128h-160V88zM115.9 176h280.2L464 243.9V296h-88V279.1C376 266.7 365.3 256 352 256s-24 10.75-24 23.1V296h-144V279.1C184 266.7 173.3 256 160 256C146.7 256 136 266.7 136 279.1V296H48V243.9L115.9 176zM448 432H64c-8.837 0-16-7.163-16-16v-72h88v16C136 373.3 146.7 384 159.1 384C173.3 384 184 373.3 184 360V344h144v16C328 373.3 338.7 384 352 384s24-10.75 24-23.1V344h88V416C464 424.8 456.8 432 448 432z"/></svg>
|
Before Width: | Height: | Size: 994 B |
1
img/activation.svg
Normal file
After Width: | Height: | Size: 7.5 KiB |
148
img/app-store-coming-soon.svg
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="livetype"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 3000 1002.812"
|
||||
enable-background="new 0 0 3000 3000"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="app-store-coming-soon.svg"
|
||||
width="3000"
|
||||
height="1002.812"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||
id="defs69" /><sodipodi:namedview
|
||||
id="namedview67"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="0.24233333"
|
||||
inkscape:cx="730.3989"
|
||||
inkscape:cy="501.37552"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1060"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="livetype" />
|
||||
<title
|
||||
id="title2">Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
|
||||
<g
|
||||
id="g64"
|
||||
transform="translate(0,-998.596)">
|
||||
<g
|
||||
id="g33">
|
||||
<g
|
||||
id="g8">
|
||||
<path
|
||||
fill="#a6a6a6"
|
||||
d="M 2761.099,998.599 H 239.036 c -9.193,0 -18.276,0 -27.445,0.05 -7.675,0.05 -15.289,0.195 -23.038,0.318 -16.834,0.198 -33.629,1.679 -50.238,4.431 -16.585,2.811 -32.652,8.109 -47.655,15.719 -14.985,7.673 -28.677,17.643 -40.58,29.549 -11.965,11.873 -21.939,25.596 -29.539,40.642 -7.62,15.016 -12.903,31.106 -15.668,47.716 -2.792,16.59 -4.293,33.37 -4.493,50.19 -0.232,7.687 -0.257,15.399 -0.38,23.087 v 579.479 c 0.123,7.784 0.147,15.325 0.38,23.112 0.199,16.82 1.701,33.6 4.493,50.188 2.757,16.62 8.042,32.72 15.668,47.741 7.596,14.997 17.571,28.665 29.539,40.471 11.857,11.959 25.559,21.937 40.58,29.551 15.003,7.629 31.068,12.96 47.655,15.813 16.612,2.729 33.405,4.211 50.238,4.433 7.749,0.171 15.363,0.269 23.038,0.269 9.169,0.05 18.252,0.05 27.445,0.05 h 2522.063 c 9.011,0 18.166,0 27.177,-0.05 7.639,0 15.473,-0.098 23.111,-0.269 16.802,-0.21 33.563,-1.691 50.141,-4.432 16.642,-2.875 32.764,-8.204 47.84,-15.814 15.007,-7.619 28.695,-17.596 40.543,-29.551 11.937,-11.853 21.934,-25.51 29.623,-40.471 7.57,-15.032 12.804,-31.131 15.521,-47.741 2.795,-16.59 4.351,-33.366 4.652,-50.188 0.098,-7.787 0.098,-15.328 0.098,-23.112 0.195,-9.108 0.195,-18.166 0.195,-27.422 v -524.686 c 0,-9.181 0,-18.289 -0.195,-27.371 0,-7.688 0,-15.4 -0.098,-23.088 -0.302,-16.822 -1.857,-33.599 -4.652,-50.19 -2.725,-16.601 -7.959,-32.689 -15.521,-47.716 -15.475,-30.158 -40.015,-54.706 -70.166,-70.192 -15.076,-7.59 -31.2,-12.888 -47.84,-15.719 -16.574,-2.763 -33.338,-4.244 -50.141,-4.432 -7.639,-0.122 -15.473,-0.269 -23.111,-0.317 -9.011,-0.051 -18.166,-0.051 -27.176,-0.051 z"
|
||||
id="path4" />
|
||||
<path
|
||||
d="m 211.713,1979.469 c -7.638,0 -15.092,-0.098 -22.67,-0.268 -15.7,-0.205 -31.362,-1.571 -46.86,-4.089 -14.451,-2.489 -28.45,-7.119 -41.535,-13.736 -12.965,-6.563 -24.791,-15.168 -35.023,-25.486 -10.38,-10.196 -19.022,-22.023 -25.584,-35.011 -6.634,-13.071 -11.225,-27.083 -13.613,-41.546 -2.579,-15.541 -3.975,-31.255 -4.174,-47.007 -0.159,-5.287 -0.367,-22.892 -0.367,-22.892 V 1210.3 c 0,0 0.222,-17.334 0.368,-22.427 0.191,-15.727 1.578,-31.417 4.15,-46.933 2.393,-14.503 6.987,-28.555 13.625,-41.67 6.538,-12.979 15.132,-24.813 25.45,-35.047 10.307,-10.33 22.169,-18.979 35.157,-25.633 13.056,-6.597 27.028,-11.193 41.449,-13.637 15.548,-2.543 31.266,-3.918 47.02,-4.113 l 22.621,-0.307 h 2576.427 l 22.892,0.319 c 15.61,0.185 31.185,1.548 46.59,4.075 14.567,2.475 28.686,7.104 41.89,13.735 26.019,13.408 47.189,34.622 60.545,60.668 6.531,13.024 11.055,26.961 13.418,41.339 2.602,15.644 4.058,31.457 4.356,47.313 0.073,7.1 0.073,14.727 0.073,22.316 0.197,9.401 0.197,18.35 0.197,27.371 v 524.686 c 0,9.108 0,17.995 -0.197,26.955 0,8.153 0,15.621 -0.098,23.308 -0.293,15.573 -1.725,31.104 -4.285,46.468 -2.339,14.567 -6.905,28.688 -13.537,41.867 -6.608,12.847 -15.201,24.57 -25.462,34.74 -10.241,10.373 -22.088,19.029 -35.083,25.634 -13.168,6.669 -27.266,11.315 -41.817,13.784 -15.496,2.53 -31.16,3.897 -46.861,4.089 -7.343,0.17 -15.032,0.268 -22.498,0.268 l -27.176,0.051 z"
|
||||
id="path6" />
|
||||
</g>
|
||||
<g
|
||||
id="_Group_">
|
||||
<g
|
||||
id="_Group_2">
|
||||
<g
|
||||
id="_Group_3">
|
||||
<path
|
||||
id="_Path_"
|
||||
fill="#ffffff"
|
||||
d="m 620.96,1507.54 c 0.548,-42.531 22.846,-81.816 59.08,-104.093 -22.963,-32.795 -60.045,-52.842 -100.06,-54.094 -42.099,-4.419 -82.912,25.191 -104.364,25.191 -21.868,0 -54.898,-24.753 -90.466,-24.021 -46.798,1.512 -89.369,27.474 -112.137,68.388 -48.486,83.945 -12.32,207.317 34.125,275.173 23.238,33.227 50.396,70.342 85.932,69.025 34.773,-1.442 47.761,-22.174 89.736,-22.174 41.585,0 53.771,22.174 90.027,21.337 37.313,-0.605 60.823,-33.374 83.245,-66.916 16.696,-23.675 29.544,-49.841 38.067,-77.529 -44.317,-18.743 -73.133,-62.169 -73.185,-110.287 z" />
|
||||
<path
|
||||
id="_Path_2"
|
||||
fill="#ffffff"
|
||||
d="m 552.478,1304.728 c 20.346,-24.424 30.369,-55.816 27.941,-87.511 -31.083,3.265 -59.794,18.12 -80.414,41.606 -20.361,23.173 -30.677,53.489 -28.674,84.271 31.5,0.325 61.408,-13.816 81.147,-38.366 z" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g30">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M 1060.525,1678.994 H 941.858 l -28.498,84.146 h -50.263 l 112.4,-311.321 h 52.221 l 112.399,311.321 h -51.12 z m -106.377,-38.829 h 94.063 l -46.37,-136.564 h -1.298 z"
|
||||
id="path14" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 1382.863,1649.664 c 0,70.534 -37.752,115.852 -94.723,115.852 -29.448,1.541 -57.187,-13.884 -71.416,-39.711 h -1.078 v 112.424 h -46.59 v -302.065 h 45.098 v 37.752 h 0.856 c 14.862,-25.666 42.63,-41.083 72.272,-40.127 57.608,-10e-4 95.581,45.536 95.581,115.875 z m -47.887,0 c 0,-45.953 -23.748,-76.165 -59.982,-76.165 -35.598,0 -59.542,30.848 -59.542,76.165 0,45.733 23.944,76.361 59.542,76.361 36.234,0 59.982,-29.991 59.982,-76.361 z"
|
||||
id="path16" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 1632.684,1649.664 c 0,70.534 -37.752,115.852 -94.723,115.852 -29.448,1.541 -57.187,-13.884 -71.416,-39.711 h -1.078 v 112.424 h -46.59 v -302.065 h 45.097 v 37.752 h 0.856 c 14.862,-25.666 42.631,-41.083 72.273,-40.127 57.608,-10e-4 95.581,45.536 95.581,115.875 z m -47.888,0 c 0,-45.953 -23.748,-76.165 -59.982,-76.165 -35.598,0 -59.542,30.848 -59.542,76.165 0,45.733 23.944,76.361 59.542,76.361 36.234,0 59.982,-29.991 59.982,-76.361 z"
|
||||
id="path18" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 1797.795,1676.399 c 3.452,30.872 33.443,51.143 74.427,51.143 39.271,0 67.523,-20.271 67.523,-48.108 0,-24.164 -17.04,-38.633 -57.388,-48.549 l -40.347,-9.72 c -57.167,-13.808 -83.706,-40.543 -83.706,-83.927 0,-53.715 46.811,-90.609 113.28,-90.609 65.784,0 110.882,36.895 112.399,90.609 h -47.031 c -2.815,-31.068 -28.498,-49.821 -66.029,-49.821 -37.532,0 -63.214,18.974 -63.214,46.59 0,22.01 16.403,34.961 56.53,44.876 l 34.3,8.422 c 63.875,15.106 90.414,40.765 90.414,86.301 0,58.244 -46.395,94.724 -120.185,94.724 -69.041,0 -115.656,-35.622 -118.668,-91.933 z"
|
||||
id="path20" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 2089.505,1482.448 v 53.715 h 43.163 v 36.895 h -43.163 v 125.131 c 0,19.439 8.642,28.498 27.616,28.498 5.124,-0.09 10.24,-0.449 15.326,-1.078 v 36.675 c -8.531,1.594 -17.201,2.315 -25.878,2.154 -45.954,0 -63.876,-17.26 -63.876,-61.28 v -130.1 h -33.002 v -36.895 h 33.002 v -53.715 z"
|
||||
id="path22" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 2157.665,1649.664 c 0,-71.416 42.062,-116.292 107.65,-116.292 65.81,0 107.675,44.875 107.675,116.292 0,71.611 -41.645,116.292 -107.675,116.292 -66.006,0 -107.65,-44.681 -107.65,-116.292 z m 167.853,0 c 0,-48.989 -22.45,-77.903 -60.202,-77.903 -37.752,0 -60.179,29.134 -60.179,77.903 0,49.186 22.427,77.879 60.179,77.879 37.752,0 60.201,-28.693 60.202,-77.879 z"
|
||||
id="path24" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 2411.402,1536.163 h 44.436 v 38.633 h 1.078 c 6.197,-24.87 28.984,-41.986 54.596,-41.009 5.369,-0.019 10.724,0.564 15.963,1.739 v 43.578 c -6.778,-2.071 -13.849,-3.021 -20.934,-2.815 -25.907,-1.051 -47.761,19.099 -48.813,45.006 -0.098,2.408 -0.01,4.821 0.264,7.216 v 134.63 h -46.591 z"
|
||||
id="path26" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 2742.284,1696.475 c -6.268,41.204 -46.395,69.481 -97.734,69.481 -66.029,0 -107.013,-44.24 -107.013,-115.215 0,-71.195 41.203,-117.369 105.055,-117.369 62.798,0 102.288,43.138 102.288,111.958 v 15.963 h -160.313 v 2.815 c -2.88,32.521 21.148,61.221 53.67,64.101 2.457,0.218 4.925,0.281 7.39,0.19 22.609,2.119 43.926,-10.864 52.418,-31.925 h 44.239 z m -157.496,-67.744 h 113.477 c 1.676,-30.101 -21.366,-55.859 -51.466,-57.536 -1.401,-0.078 -2.805,-0.102 -4.207,-0.071 -31.734,-0.189 -57.613,25.383 -57.803,57.117 0,0.163 0,0.327 0,0.49 z"
|
||||
id="path28" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="_Group_4">
|
||||
<g
|
||||
id="g61">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata71"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</dc:title></cc:Work></rdf:RDF></metadata><text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none"
|
||||
x="1046.0166"
|
||||
y="312.38776"
|
||||
id="text4830"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4828"
|
||||
x="1046.0166"
|
||||
y="312.38776"
|
||||
style="font-size:192px;fill:#ffffff;fill-opacity:1"
|
||||
dx="0">Coming soon to</tspan></text></svg>
|
After Width: | Height: | Size: 9.6 KiB |
45
img/appstore.svg
Normal file
|
@ -0,0 +1,45 @@
|
|||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
|
||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
</g>
|
||||
<g id="_Group_" data-name="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
|
||||
<path id="_Path_2" data-name="<Path>" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
|
||||
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
|
||||
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
|
||||
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
|
||||
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<g>
|
||||
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
|
||||
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
|
||||
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
|
||||
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
|
||||
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
BIN
img/bitcoin-p2p.png
Normal file
After Width: | Height: | Size: 187 KiB |
1
img/bitcoin-p2p.svg
Normal file
After Width: | Height: | Size: 5.3 KiB |
1
img/bitcoin-phone.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Filled_outline" data-name="Filled outline"><g fill="#195c85"><path d="m46.707 18.707-1.414-1.414 3-3a1 1 0 0 1 .707-.293h8v2h-7.586z"/><path d="m17.293 20.707-4.707-4.707h-5.586v-2h6a1 1 0 0 1 .707.293l5 5z"/><path d="m13 45h-6v-2h5.586l4.707-4.707 1.414 1.414-5 5a1 1 0 0 1 -.707.293z"/><path d="m17.628 29.929-4.82-1.929h-6.808v-2h7a1.012 1.012 0 0 1 .372.071l5 2z"/><path d="m17.628 37.929-4.82-1.929h-6.808v-2h7a1.012 1.012 0 0 1 .372.071l5 2z"/><path d="m46.515 27.857-1.03-1.714 5-3a1 1 0 0 1 .515-.143h7v2h-6.723z"/><path d="m58 36h-7a1 1 0 0 1 -.515-.143l-5-3 1.03-1.714 4.762 2.857h6.723z"/><path d="m58 44h-7a1 1 0 0 1 -.515-.143l-5-3 1.03-1.714 4.762 2.857h6.723z"/></g><rect fill="#767f87" height="58" rx="4" transform="matrix(-1 0 0 -1 64 64)" width="28" x="18" y="3"/><circle cx="32" cy="55" fill="#394d5c" r="2"/><path d="m30 6h8v2h-8z" fill="#394d5c"/><path d="m26 6h2v2h-2z" fill="#394d5c"/><path d="m18 11h28v38h-28z" fill="#f4f4e6"/><path d="m23 31h2v12h-2z" fill="#7fcac9"/><path d="m39 31h2v13h-2z" fill="#7fcac9"/><path d="m31 35h2v10h-2z" fill="#7fcac9"/><path d="m35 34h2v8h-2z" fill="#7fcac9"/><path d="m27 34h2v7h-2z" fill="#7fcac9"/><path d="m27 43h2v2h-2z" fill="#7fcac9"/><path d="m35 44h2v2h-2z" fill="#7fcac9"/><circle cx="5" cy="15" fill="#32b1cc" r="2"/><circle cx="5" cy="27" fill="#32b1cc" r="2"/><circle cx="5" cy="35" fill="#32b1cc" r="2"/><circle cx="5" cy="44" fill="#32b1cc" r="2"/><circle cx="59" cy="15" fill="#32b1cc" r="2"/><circle cx="59" cy="24" fill="#32b1cc" r="2"/><circle cx="59" cy="35" fill="#32b1cc" r="2"/><circle cx="59" cy="43" fill="#32b1cc" r="2"/><path d="m9 4h2v2h-2z" fill="#d9176c"/><path d="m9 8h2v2h-2z" fill="#d9176c"/><path d="m7 6h2v2h-2z" fill="#d9176c"/><path d="m11 6h2v2h-2z" fill="#d9176c"/><path d="m53 49h2v2h-2z" fill="#d9176c"/><path d="m53 53h2v2h-2z" fill="#d9176c"/><path d="m51 51h2v2h-2z" fill="#d9176c"/><path d="m55 51h2v2h-2z" fill="#d9176c"/><circle cx="32" cy="25" fill="#ffd782" r="10"/><path d="m33 18h2v3h-2z" fill="#e97424"/><path d="m29 18h2v3h-2z" fill="#e97424"/><path d="m29 29h2v3h-2z" fill="#e97424"/><path d="m33 29h2v3h-2z" fill="#e97424"/><path d="m35 26h-5a1 1 0 0 1 -1-1v-4a1 1 0 0 1 1-1h5a3 3 0 0 1 0 6zm-4-2h4a1 1 0 0 0 0-2h-4z" fill="#e97424"/><path d="m35 30h-5a1 1 0 0 1 -1-1v-4a1 1 0 0 1 1-1h5a3 3 0 0 1 0 6zm-4-2h4a1 1 0 0 0 0-2h-4z" fill="#e97424"/><path d="m27 20h3v2h-3z" fill="#e97424"/><path d="m27 28h4v2h-4z" fill="#e97424"/><path d="m32 52a3 3 0 1 0 3 3 3 3 0 0 0 -3-3zm0 4a1 1 0 1 1 1-1 1 1 0 0 1 -1 1z"/><path d="m30 6h8v2h-8z"/><path d="m26 6h2v2h-2z"/><path d="m32 14a10.985 10.985 0 0 0 -9 17.305v11.695h2v-9.521a11.013 11.013 0 0 0 2 1.307v6.214h2v-5.426a10.9 10.9 0 0 0 2 .375v9.051h2v-9.051a10.9 10.9 0 0 0 2-.375v6.426h2v-7.214a11.013 11.013 0 0 0 2-1.307v10.521h2v-12.695a10.985 10.985 0 0 0 -9-17.305zm0 20a9 9 0 1 1 9-9 9.011 9.011 0 0 1 -9 9z"/><path d="m35 20v-2h-2v2h-2v-2h-2v2h-2v2h2v6h-2v2h2v2h2v-2h2v2h2v-2a2.987 2.987 0 0 0 2.22-5 2.987 2.987 0 0 0 -2.22-5zm1 3a1 1 0 0 1 -1 1h-4v-2h4a1 1 0 0 1 1 1zm-1 5h-4v-2h4a1 1 0 0 1 0 2z"/><path d="m27 43h2v2h-2z"/><path d="m35 44h2v2h-2z"/><path d="m49.414 16h6.77a3 3 0 1 0 0-2h-7.184a1 1 0 0 0 -.707.293l-1.293 1.293v-8.586a5.006 5.006 0 0 0 -5-5h-20a5.006 5.006 0 0 0 -5 5v10.586l-3.293-3.293a1 1 0 0 0 -.707-.293h-5.184a3 3 0 1 0 0 2h4.77l4.414 4.414v7.109l-3.628-1.452a1.012 1.012 0 0 0 -.372-.071h-5.184a3 3 0 1 0 0 2h4.992l4.192 1.677v5.846l-3.628-1.452a1.012 1.012 0 0 0 -.372-.071h-5.184a3 3 0 1 0 0 2h4.992l4.192 1.677v.909l-4.414 4.414h-4.77a3 3 0 1 0 0 2h5.184a1 1 0 0 0 .707-.293l3.293-3.293v15.586a5.006 5.006 0 0 0 5 5h20a5.006 5.006 0 0 0 5-5v-15.234l3.485 2.091a1 1 0 0 0 .515.143h5.184a3 3 0 1 0 0-2h-4.907l-4.277-2.566v-5.668l3.485 2.091a1 1 0 0 0 .515.143h5.184a3 3 0 1 0 0-2h-4.907l-4.277-2.566v-3.868l4.277-2.566h4.907a3 3 0 1 0 0-2h-5.184a1 1 0 0 0 -.515.143l-3.485 2.091v-6.82zm9.586-2a1 1 0 1 1 -1 1 1 1 0 0 1 1-1zm-54 2a1 1 0 1 1 1-1 1 1 0 0 1 -1 1zm0 12a1 1 0 1 1 1-1 1 1 0 0 1 -1 1zm0 8a1 1 0 1 1 1-1 1 1 0 0 1 -1 1zm0 9a1 1 0 1 1 1-1 1 1 0 0 1 -1 1zm54-3a1 1 0 1 1 -1 1 1 1 0 0 1 1-1zm0-8a1 1 0 1 1 -1 1 1 1 0 0 1 1-1zm0-11a1 1 0 1 1 -1 1 1 1 0 0 1 1-1zm-40-11h26v36h-26zm3-8h20a3 3 0 0 1 3 3v3h-26v-3a3 3 0 0 1 3-3zm20 56h-20a3 3 0 0 1 -3-3v-7h26v7a3 3 0 0 1 -3 3z"/><path d="m9 4h2v2h-2z"/><path d="m9 8h2v2h-2z"/><path d="m7 6h2v2h-2z"/><path d="m11 6h2v2h-2z"/><path d="m53 49h2v2h-2z"/><path d="m53 53h2v2h-2z"/><path d="m51 51h2v2h-2z"/><path d="m55 51h2v2h-2z"/></g></svg>
|
After Width: | Height: | Size: 4.5 KiB |
1
img/bot.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 60 60" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Page-1" fill="none" fill-rule="evenodd"><g id="002---Bot-Message" fill-rule="nonzero" transform="translate(-1)"><path id="Shape" d="m48 39c0 15-10.3 20-23 20s-23-5-23-20c0-14 10.3-24 23-24 3.0903976-.0100698 6.1511982.6020903 9 1.8v1.2c0 1.6568542 1.3431458 3 3 3h1c.5522847 0 1 .4477153 1 1v3.23c.0000627.3593049.1928829.6909565.505101.8687761.3122182.1778196.6958342.1744694 1.004899-.0087761l2.93-1.76c3.0318572 4.2853551 4.6282198 9.4210217 4.56 14.67z" fill="#f5f5f5"/><path id="Shape" d="m43.44 24.33-2.146 1.289c2.4703306 4.0225352 3.754925 8.6607321 3.706 13.381 0 14.4-9.5 19.584-21.5 19.973.5.016 1 .027 1.5.027 12.7 0 23-5 23-20 .0682198-5.2489783-1.5281428-10.3846449-4.56-14.67z" fill="#cfd8dc"/><rect id="Rectangle-path" fill="#607d8b" height="22" rx="11" width="38" x="6" y="28"/><path id="Shape" d="m33 28h-3c6.0751322 0 11 4.9248678 11 11s-4.9248678 11-11 11h3c6.0751322-.0000001 10.9999998-4.9248678 10.9999998-11s-4.9248676-10.9999999-10.9999998-11z" fill="#37474f"/><circle id="Oval" cx="14" cy="38" fill="#00bcd4" r="3"/><circle id="Oval" cx="36" cy="38" fill="#00bcd4" r="3"/><circle id="Oval" cx="8" cy="13" fill="#02a9f4" r="2"/><path id="Shape" d="m60 4v14c0 1.6568542-1.3431458 3-3 3h-7.72c-.1824524.0014388-.3614878.0496407-.52.14l-5.32 3.19-2.93 1.76c-.3090648.1832455-.6926808.1865957-1.004899.0087761-.3122181-.1778196-.5050383-.5094712-.505101-.8687761v-3.23c0-.5522847-.4477153-1-1-1h-1c-1.6568542 0-3-1.3431458-3-3v-14c0-1.65685425 1.3431458-3 3-3h20c1.6568542 0 3 1.34314575 3 3z" fill="#ffdf00"/><path id="Shape" d="m57 1h-3c1.6568542 0 3 1.34314575 3 3v14c0 1.6568542-1.3431458 3-3 3h3c1.6568542 0 3-1.3431458 3-3v-14c0-1.65685425-1.3431458-3-3-3z" fill="#fec108"/><g fill="#000"><path id="Shape" d="m17 27c-6.627417 0-12 5.372583-12 12s5.372583 12 12 12h16c6.627417 0 12-5.372583 12-12s-5.372583-12-12-12zm26 12c-.0060624 5.5203344-4.4796656 9.9939376-10 10h-16c-5.5228475 0-10-4.4771525-10-10s4.4771525-10 10-10h16c5.5203344.0060624 9.9939376 4.4796656 10 10z"/><path id="Shape" d="m14 34c-2.209139 0-4 1.790861-4 4s1.790861 4 4 4 4-1.790861 4-4-1.790861-4-4-4zm0 6c-1.1045695 0-2-.8954305-2-2s.8954305-2 2-2 2 .8954305 2 2-.8954305 2-2 2z"/><path id="Shape" d="m36 42c2.209139 0 4-1.790861 4-4s-1.790861-4-4-4-4 1.790861-4 4 1.790861 4 4 4zm0-6c1.1045695 0 2 .8954305 2 2s-.8954305 2-2 2-2-.8954305-2-2 .8954305-2 2-2z"/><path id="Shape" d="m22 25h6c.5522847 0 1-.4477153 1-1s-.4477153-1-1-1h-6c-.5522847 0-1 .4477153-1 1s.4477153 1 1 1z"/><path id="Shape" d="m24 22h2c.5522847 0 1-.4477153 1-1s-.4477153-1-1-1h-2c-.5522847 0-1 .4477153-1 1s.4477153 1 1 1z"/><path id="Shape" d="m57 0h-20c-2.209139 0-4 1.790861-4 4v11.347c-2.5712007-.8985807-5.276313-1.3540539-8-1.347-5.9058605-.0610987-11.6181844 2.1038721-16 6.064v-4.248c1.3775467-.4870363 2.2038721-1.8956662 1.9567957-3.3357331-.2470764-1.4400668-1.4956868-2.49269402-2.9567957-2.49269402s-2.70971927 1.05262722-2.95679568 2.49269402c-.24707641 1.4400669.57924899 2.8486968 1.95679568 3.3357331v6.3c-3.96356752 4.7243441-6.09348037 10.7179189-6 16.884 0 13.738 8.3 21 24 21s24-7.262 24-21c.0508124-5.0845422-1.3914304-10.0722404-4.148-14.345l4.425-2.655h7.723c2.209139 0 4-1.790861 4-4v-14c0-2.209139-1.790861-4-4-4zm-49 12c.55228475 0 1 .4477153 1 1s-.44771525 1-1 1-1-.4477153-1-1 .44771525-1 1-1zm39 27c0 12.607-7.4 19-22 19s-22-6.393-22-19c0-13.112 9.458-23 22-23 2.7368223-.0026943 5.4500512.5060361 8 1.5v.5c0 2.209139 1.790861 4 4 4h1v3.234c.0049282 1.1025217.8974783 1.9950718 2 2 .3627718-.0009.7184675-.1004533 1.029-.288l2.108-1.265c2.5710617 3.9624335 3.9149107 8.595808 3.863 13.319zm12-21c0 1.1045695-.8954305 2-2 2h-7.723c-.3624418.0003443-.7180261.0988297-1.029.285l-8.248 4.949v-3.234c0-1.1045695-.8954305-2-2-2h-1c-1.1045695 0-2-.8954305-2-2v-14c0-1.1045695.8954305-2 2-2h20c1.1045695 0 2 .8954305 2 2z"/><path id="Shape" d="m27.3 43.282c-.6559217.5070896-1.4720819.7618735-2.3.718-.8276815.0432642-1.643525-.2110662-2.3-.717-.3985064-.3506401-1.0015362-.3288293-1.37367.049684-.3721338.3785132-.3836925.9818266-.02633 1.374316 1.023374.8869696 2.346923 1.3494963 3.7 1.293 1.35546.0583036 2.6818413-.4043376 3.707-1.293.3854708-.3870661.3854708-1.0129339 0-1.4-.3841726-.3895471-1.0092311-.4006534-1.407-.025z"/><path id="Shape" d="m55 6h-16c-.5522847 0-1 .44771525-1 1s.4477153 1 1 1h16c.5522847 0 1-.44771525 1-1s-.4477153-1-1-1z"/><path id="Shape" d="m39 12h5c.5522847 0 1-.4477153 1-1s-.4477153-1-1-1h-5c-.5522847 0-1 .4477153-1 1s.4477153 1 1 1z"/><path id="Shape" d="m52 14h-13c-.5522847 0-1 .4477153-1 1s.4477153 1 1 1h13c.5522847 0 1-.4477153 1-1s-.4477153-1-1-1z"/></g></g></g></svg>
|
After Width: | Height: | Size: 4.6 KiB |
1
img/communication.svg
Normal file
After Width: | Height: | Size: 7.3 KiB |
186
img/damus-nobg.svg
Normal file
|
@ -0,0 +1,186 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="146.15311mm"
|
||||
height="184.664mm"
|
||||
viewBox="0 0 146.15311 184.66401"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)"
|
||||
sodipodi:docname="damus-nobg.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="73.992831"
|
||||
inkscape:cy="206.8436"
|
||||
inkscape:window-width="1435"
|
||||
inkscape:window-height="844"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer2" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true"
|
||||
style="display:none"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Logo"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
184
img/damus.svg
Normal file
|
@ -0,0 +1,184 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256mm"
|
||||
height="256mm"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
sodipodi:docname="damus.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="406.11975"
|
||||
inkscape:cy="491.88416"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1060"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
190
img/damus_notif.svg
Normal file
|
@ -0,0 +1,190 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256mm"
|
||||
height="256mm"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
sodipodi:docname="damus_notif.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="407.8014"
|
||||
inkscape:cy="491.88416"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1060"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
<circle
|
||||
style="fill:#ff2a2a;stroke-width:1.5875;stroke-linejoin:round;paint-order:stroke fill markers"
|
||||
id="path1029"
|
||||
cx="169.51305"
|
||||
cy="171.74377"
|
||||
r="66.864555" />
|
||||
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
BIN
img/digital-nomad.png
Normal file
After Width: | Height: | Size: 146 KiB |
1
img/digital-nomad.svg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
img/encrypted-message.png
Normal file
After Width: | Height: | Size: 94 KiB |
1
img/freelance.svg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
img/logo.png
Normal file
After Width: | Height: | Size: 143 KiB |
1
img/message.svg
Normal file
After Width: | Height: | Size: 8.4 KiB |
1
img/protection.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_5" enable-background="new 0 0 64 64" height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><g><path d="m6 16c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h42c2.21 0 4-1.79 4-4v-32z" fill="#ccd1d9"/></g><g><path d="m62 8v7.88c0 4.36-1.17 8.56-3.29 12.21-1.67 2.89-3.95 5.44-6.71 7.44-.72.54-1.48 1.03-2.26 1.48-.55.31-1.13.55-1.74.72-.65.18-1.32.27-2 .27-1.31 0-2.6-.34-3.74-.99-3.82-2.19-6.9-5.32-9.02-9.01-1.42-2.46-2.4-5.17-2.88-8-.23-1.31-.35-2.65-.36-4 0-.04 0-.08 0-.12v-7.88c0-1.1.9-2 2-2h.79c4.7 0 9.3-1.39 13.21-4 3.91 2.61 8.51 4 13.21 4h.79c1.1 0 2 .9 2 2z" fill="#aab2bd"/></g><g><path d="m46.14 6.73c-.05-.02-.09-.05-.14-.07-3.7 2-7.78 3.13-12 3.31v5.91c0 7.28 3.93 14.04 10.25 17.66.53.3 1.14.46 1.75.46s1.22-.16 1.75-.46c6.32-3.62 10.25-10.38 10.25-17.66v-5.91c-4.17-.18-8.2-1.28-11.86-3.24z" fill="#fcd770"/></g><g><path d="m54 13v10l-6.67-5z" fill="#dfb28b"/></g><g><path d="m54 13-6.67 5-1.33 1-1.33-1-6.67-5h8z" fill="#cf9e76"/></g><g><path d="m54 23h-8-8l6.67-5 1.33 1 1.33-1z" fill="#d3a06c"/></g><g><path d="m48 37.73v5.27c0 .55-.45 1-1 1h-40c-.55 0-1-.45-1-1v-15h27.24c2.12 3.69 5.2 6.82 9.02 9.01 1.14.65 2.43.99 3.74.99.68 0 1.35-.09 2-.27z" fill="#f0d0b4"/></g><g><path d="m44.67 18-6.67 5v-10z" fill="#dfb28b"/></g><g><path d="m42 62h-30c0-1.1.45-2.1 1.17-2.83.73-.72 1.73-1.17 2.83-1.17h2 18 2c2.21 0 4 1.79 4 4z" fill="#969faa"/></g><g><path d="m34 52 2 6h-18l2-6z" fill="#aab2bd"/></g><g><path d="m33.24 28h-27.24v-7c0-.55.45-1 1-1h23.36c.48 2.83 1.46 5.54 2.88 8z" fill="#b4dd7f"/></g><g><path d="m10 32h8v8h-8z" fill="#ff826e"/></g></g><g><path d="m41 47h8v2h-8z"/><path d="m37 47h2v2h-2z"/><path d="m46.476 5.784-.476-.257-.476.257c-3.591 1.941-7.483 3.015-11.567 3.19l-.957.042v6.86c0 7.639 4.12 14.738 10.753 18.527.684.391 1.46.597 2.247.597.786 0 1.563-.206 2.248-.597 6.632-3.79 10.752-10.888 10.752-18.527v-6.86l-.957-.041c-4.085-.176-7.977-1.249-11.567-3.191zm-5.476 16.216 3.667-2.75 1.333 1 1.333-1 3.667 2.75zm-2-7 4 3-4 3zm14 6-4-3 4-3zm-7-3.25-5-3.75h10zm-11-1.874v-4.96c3.492-.264 6.844-1.152 10-2.635v3.719h-8v12h8v8.77c-.084-.036-.175-.057-.255-.103-6.011-3.435-9.745-9.868-9.745-16.791zm22 0c0 6.923-3.734 13.356-9.743 16.79-.081.046-.172.067-.257.104v-8.77h8v-12h-8v-3.719c3.156 1.483 6.508 2.371 10 2.635z"/><path d="m60 5h-.789c-4.52 0-8.896-1.325-12.656-3.832l-.555-.37-.555.37c-3.759 2.507-8.136 3.832-12.656 3.832h-.789c-1.654 0-3 1.346-3 3v7h-23c-2.757 0-5 2.243-5 5v28c0 2.757 2.243 5 5 5h12.612l-1.333 4h-1.279c-2.757 0-5 2.243-5 5v1h32v-1c0-2.757-2.243-5-5-5h-1.279l-1.333-4h12.612c2.757 0 5-2.243 5-5v-11.959c6.237-4.751 10-12.211 10-20.165v-7.876c0-1.654-1.346-3-3-3zm-53 24h25.687c2.196 3.623 5.288 6.708 9.081 8.876 1.286.735 2.749 1.124 4.232 1.124.335 0 .669-.026 1-.065v4.065h-40zm0-2v-6h22.53c.434 2.089 1.128 4.103 2.057 6zm33.829 34h-27.658c.413-1.164 1.525-2 2.829-2h22c1.304 0 2.416.836 2.829 2zm-6.217-4h-15.224l1.333-4h12.558zm16.388-9c0 1.654-1.346 3-3 3h-42c-1.654 0-3-1.346-3-3v-28c0-1.654 1.346-3 3-3h23.038c.03.671.081 1.339.164 2h-22.202c-1.103 0-2 .897-2 2v22c0 1.103.897 2 2 2h40c1.103 0 2-.897 2-2v-4.559c.423-.159.838-.34 1.232-.565.262-.15.512-.315.768-.473zm10-32.124c0 8.354-4.506 16.119-11.76 20.264-1.97 1.125-4.511 1.125-6.48 0-7.254-4.145-11.76-11.91-11.76-20.264v-7.876c0-.552.449-1 1-1h.789c4.695 0 9.249-1.314 13.211-3.809 3.962 2.495 8.516 3.809 13.211 3.809h.789c.551 0 1 .448 1 1z"/><path d="m13 23h2v2h-2z"/><path d="m17 23h2v2h-2z"/><path d="m9 23h2v2h-2z"/><path d="m9 41h10v-10h-10zm2-8h6v6h-6z"/><path d="m21 31h2v2h-2z"/><path d="m25 31h7v2h-7z"/><path d="m21 35h11v2h-11z"/><path d="m21 39h11v2h-11z"/></g></g></svg>
|
After Width: | Height: | Size: 3.6 KiB |
1
img/social-media.svg
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
img/ss.png
Normal file
After Width: | Height: | Size: 750 KiB |
50
img/testflight.svg
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
img/undercover.png
Normal file
After Width: | Height: | Size: 124 KiB |
1
img/undercover.svg
Normal file
After Width: | Height: | Size: 6.5 KiB |
355
index.html
|
@ -1,288 +1,83 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#0f0f0f"/>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; manifest-src 'self'; connect-src 'self' ws: wss:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self' fonts.googleapis.com; img-src http: https: data:; media-src *; font-src 'self' fonts.gstatic.com; child-src 'none';" />
|
||||
<title>Yo, Sup</title>
|
||||
<link rel="manifest" href="/pwa/manifest.json"/>
|
||||
<link rel="icon" href="/icon/icon.svg" type="image/svg+xml"/>
|
||||
<link rel="apple-touch-icon" href="/pwa/icon-256.png"/>
|
||||
<link rel="stylesheet" href="/css/vars.css?v=1">
|
||||
<link rel="stylesheet" href="/css/utils.css?v=1">
|
||||
<link rel="stylesheet" href="/css/styles.css?v=13">
|
||||
<link rel="stylesheet" href="/css/responsive.css?v=10">
|
||||
<script defer src="/js/util.js?v=5"></script>
|
||||
<script defer src="/js/ui/safe-html.js?v=1"></script>
|
||||
<script defer src="/js/ui/util.js?v=8"></script>
|
||||
<script defer src="/js/ui/render.js?v=15"></script>
|
||||
<script defer src="/js/ui/state.js?v=1"></script>
|
||||
<script defer src="/js/ui/fmt.js?v=1"></script>
|
||||
<script defer src="/js/ui/profile.js?v=1"></script>
|
||||
<script defer src="/js/ui/settings.js?v=1"></script>
|
||||
<script defer src="/js/ui/dm.js?v=1"></script>
|
||||
<script defer src="/js/nostr.js?v=7"></script>
|
||||
<script defer src="/js/core.js?v=1"></script>
|
||||
<script defer src="/js/model.js?v=1"></script>
|
||||
<script defer src="/js/contacts.js?v=1"></script>
|
||||
<script defer src="/js/event.js?v=1"></script>
|
||||
<script defer src="/js/lib.js?v=1"></script>
|
||||
<script defer src="/js/main.js?v=1"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container-busy">
|
||||
<div class="loader" title="Loading...">
|
||||
<img class="dark-invert" src="/icon/loader-fragment.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="container-welcome" class="hide">
|
||||
<div class="hero-box">
|
||||
<div class="padded">
|
||||
<h1>
|
||||
Yo, Sup?
|
||||
<img class="icon svg" src="/icon/logo-inverted.svg"/>
|
||||
</h1>
|
||||
<p>A minimal experience for Nostr.</p>
|
||||
<p>Please access with a nos2x compatible browser.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>damus</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<div id="container-app" class="hide">
|
||||
|
||||
<div id="container">
|
||||
<div class="flex-fill vertical-hide"></div>
|
||||
<nav id="nav" class="nav full flex-noshrink vertical-hide">
|
||||
<div>
|
||||
<div id="app-icon-logo">
|
||||
<img class="icon svg" title="Damus" src="/icon/logo-inverted.svg"/>
|
||||
</div>
|
||||
<button action="open-view" data-view="friends" class="nav icon"
|
||||
title="Home">
|
||||
<img class="icon svg inactive" src="/icon/home.svg"/>
|
||||
<img class="icon svg active" src="/icon/home-active.svg"/>
|
||||
</button>
|
||||
<button action="open-view" data-view="dm" class="nav icon"
|
||||
title="Direct Messages">
|
||||
<img class="icon svg inactive" src="/icon/messages.svg"/>
|
||||
<img class="icon svg active" src="/icon/messages-active.svg"/>
|
||||
<div class="new-notifications hide" role="dm"></div>
|
||||
</button>
|
||||
<button action="open-view" data-view="notifications"
|
||||
class="nav icon" title="Notifications">
|
||||
<img class="icon svg inactive" src="/icon/notifications.svg"/>
|
||||
<img class="icon svg active" src="/icon/notifications-active.svg"/>
|
||||
<div class="new-notifications hide" role="activity"></div>
|
||||
</button>
|
||||
<button action="open-view" data-view="settings"
|
||||
title="Settings" class="nav icon">
|
||||
<img class="icon svg inactive" src="/icon/settings.svg"/>
|
||||
<img class="icon svg active" src="/icon/settings-active.svg"/>
|
||||
</button>
|
||||
<button action="new-note" title="New Note" class="nav icon new-note">
|
||||
<img class="icon svg invert" src="/icon/new-note.svg"/>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<meta property="og:title" content="Damus">
|
||||
<meta property="og:description" content="A new social network that you control">
|
||||
<meta property="og:image" content="https://damus.io/img/logo.png">
|
||||
<meta property="og:url" content="https://damus.io">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="https://damus.io/img/logo.png">
|
||||
|
||||
<div id="view">
|
||||
<header>
|
||||
<label>Home</label>
|
||||
<div id="header-tools">
|
||||
<button class="action small hide"
|
||||
disabled action="mark-all-read">
|
||||
Mark All Read
|
||||
</button>
|
||||
<img class="pfp hide" role="their-pfp" data-pubkey=""
|
||||
src="/icon/no-user.svg"/>
|
||||
<img class="pfp hide" role="my-pfp" data-pubkey=""
|
||||
src="/icon/no-user.svg"/>
|
||||
</div>
|
||||
</header>
|
||||
<div id="profile-info" role="profile-info" class="bottom-border hide">
|
||||
<div class="profile-banner" name="profile-banner"></div>
|
||||
<div class="flex">
|
||||
<img name="profile-image" class="pfp jumbo hide"/>
|
||||
<label name="profile-nip05"></label>
|
||||
<div class="profile-tools">
|
||||
<button class="icon link hide"
|
||||
name="profile-website" action="open-link">
|
||||
<img class="icon svg" src="/icon/profile-website.svg"/>
|
||||
</button>
|
||||
<button class="icon link hide"
|
||||
title="Copy Lightning Address"
|
||||
name="profile-lud06" action="open-lud06">
|
||||
<img class="icon svg" src="/icon/profile-zap.svg"/>
|
||||
</button>
|
||||
<button class="icon" name="message-user"
|
||||
title="Directly Message">
|
||||
<img class="icon svg" src="/icon/message-user.svg"/>
|
||||
</button>
|
||||
<button class="icon" name="copy-pk"
|
||||
data-pk="" title="Copy Public Key">
|
||||
<img class="icon svg" src="/icon/pubkey.svg"/></button>
|
||||
|
||||
<button class="action" name="follow-user"
|
||||
data-pk="">Follow</button>
|
||||
<button class="action" name="edit-profile"
|
||||
title="Update Profile">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p name="profile-about"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-events">
|
||||
<div class="loader" title="Loading...">
|
||||
<img class="dark-invert" src="/icon/loader-fragment.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-new" class="show-new bottom-border hide">
|
||||
<button action="show-timeline-new">
|
||||
Show New (<span role="count">0</span>)</button>
|
||||
</div>
|
||||
<div id="dms" class="hide">
|
||||
</div>
|
||||
<div id="timeline" class="events"></div>
|
||||
<div id="show-more" class="show-more">
|
||||
<button action="show-timeline-more">Show More</button>
|
||||
</div>
|
||||
<div id="settings" class="hide">
|
||||
<section>
|
||||
<header>
|
||||
<label>Relays</label>
|
||||
<button id="add-relay" class="btn-text">
|
||||
<img class="svg icon small" src="/icon/add-relay.svg"/>
|
||||
</button>
|
||||
</header>
|
||||
<table id="relay-list" class="row">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>Option</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section>
|
||||
<header><label>Info</label></header>
|
||||
<p>
|
||||
<a href="https://git.sr.ht/~tomtom/damus">Source Code</a>
|
||||
<a href="https://todo.sr.ht/~tomtom/damus-web-issues">Bug Tracker</a>
|
||||
<a href="mailto:thomas.c.mathews@gmail.com">Email Me</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<div id="dm-post" class="hide">
|
||||
<textarea class="post-input dm" name="message"></textarea>
|
||||
<div class="post-tools">
|
||||
<button name="send-dm" class="action">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav mobile">
|
||||
<button action="open-view" data-view="friends" class="icon"
|
||||
title="Home">
|
||||
<img class="icon svg inactive" src="/icon/home.svg"/>
|
||||
<img class="icon svg active" src="/icon/home-active.svg"/>
|
||||
</button>
|
||||
<button action="open-view" data-view="dm" class="icon"
|
||||
title="Direct Messages">
|
||||
<img class="icon svg inactive" src="/icon/messages.svg"/>
|
||||
<img class="icon svg active" src="/icon/messages-active.svg"/>
|
||||
<div class="new-notifications hide" role="dm"></div>
|
||||
</button>
|
||||
<button action="open-view" data-view="notifications"
|
||||
class="icon" title="Notifications">
|
||||
<img class="icon svg inactive" src="/icon/notifications.svg"/>
|
||||
<img class="icon svg active" src="/icon/notifications-active.svg"/>
|
||||
<div class="new-notifications hide" role="activity"></div>
|
||||
</button>
|
||||
<button action="open-view" data-view="settings"
|
||||
title="Settings" class="icon">
|
||||
<img class="icon svg inactive" src="/icon/settings.svg"/>
|
||||
<img class="icon svg active" src="/icon/settings-active.svg"/>
|
||||
</button>
|
||||
<button id="new-note-mobile" action="new-note"
|
||||
title="New Note" class="nav icon new-note">
|
||||
<img class="icon svg invert" src="/icon/new-note.svg"/>
|
||||
</button>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="flex-fill vertical-hide"></div>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="css/normalize.css">
|
||||
<link rel="stylesheet" href="css/skeleton.css?v=2">
|
||||
<link rel="stylesheet" href="css/custom.css?v=5">
|
||||
|
||||
<dialog id="media-preview" action="close-media">
|
||||
<img action="close-media" src=""/>
|
||||
<!-- TODO add loader to media preview -->
|
||||
</dialog>
|
||||
<dialog id="reply-modal">
|
||||
<div class="container">
|
||||
<header>
|
||||
<label>Reply To</label>
|
||||
<button class="icon" action="close-modal">
|
||||
<img class="icon svg" src="/icon/close-modal.svg"/>
|
||||
</button>
|
||||
</header>
|
||||
<div id="replying-to"></div>
|
||||
<div id="replybox">
|
||||
<textarea id="reply-content" class="post-input"
|
||||
placeholder="Reply..."></textarea>
|
||||
<div class="post-tools new">
|
||||
<button class="action" name="send">Send</button>
|
||||
</div>
|
||||
<div class="post-tools reply">
|
||||
<button class="action" name="reply-all" data-all="1">Reply All</button>
|
||||
<button class="action" name="reply">Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="profile-editor">
|
||||
<div class="container">
|
||||
<header>
|
||||
<label>Update Profile</label>
|
||||
<button class="icon" action="close-modal">
|
||||
<img class="icon svg" src="/icon/close-modal.svg"/>
|
||||
</button>
|
||||
</header>
|
||||
<div>
|
||||
<input type="text" class="block w100" name="name" placeholder="Name"/>
|
||||
<input type="text" class="block w100" name="display_name" placeholder="Display Name"/>
|
||||
<input type="text" class="block w100" name="picture" placeholder="Picture URL"/>
|
||||
<input type="text" class="block w100" name="banner" placeholder="Banner URL"/>
|
||||
<input type="text" class="block w100" name="website" placeholder="Website"/>
|
||||
<input type="text" class="block w100" name="lud06" placeholder="lud06"/>
|
||||
<input type="text" class="block w100" name="nip05" placeholder="nip05"/>
|
||||
<textarea name="about" class="block w100" placeholder="A bit about you."></textarea>
|
||||
<button class="action float-right" action="open-profile-editor">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="event-details">
|
||||
<div class="container">
|
||||
<header>
|
||||
<label>Event Details</label>
|
||||
<button class="icon modal-floating-close-btn" action="close-modal">
|
||||
<img class="icon svg" src="/icon/close-modal.svg"/>
|
||||
</button>
|
||||
</header>
|
||||
<div class="max-content">
|
||||
<pre><code></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="img/damus-nobg.svg"/>
|
||||
</span>
|
||||
<span class="damus">damus</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<section class="hero">
|
||||
<h2 class="title">The social network you control
|
||||
<h5 class="subtitle">Your very own Twitter for your friends or business.</h5>
|
||||
|
||||
<img style="width: 50%; height: 50%" src="img/ss.png" />
|
||||
|
||||
<div class="value-props row">
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/digital-nomad.svg">
|
||||
<b>You are in control</b>. Built on open internet protocols, there is no platform that can ban or censor you. You are in control of your data & speech.
|
||||
</div>
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/message.svg">
|
||||
<b>Encrypted</b>. End-to-End encrypted private messaging. Keep big tech out of your DMs.
|
||||
</div>
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/undercover.svg">
|
||||
<b>No registration required</b>. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.
|
||||
</div>
|
||||
</div>
|
||||
<div class="value-props row">
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/social-media.svg">
|
||||
<b>No servers required.</b> Messages are distributed via decentralized relays. No need to run any infrastructure and there are no single points of failure. Simple!
|
||||
</div>
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/bot.svg">
|
||||
<b>Programmable.</b> Easily integrate bots that automate your life or business. Get notified when your servers go down, retweet to your team and collaborate in realtime.
|
||||
</div>
|
||||
<div class="four columns value-prop">
|
||||
<img class="value-img" src="img/bitcoin-phone.svg">
|
||||
<b>Earn money</b>. Tip your friend's posts and stack sats with Bitcoin & ⚡️, the native currency of the internet.</div>
|
||||
</div>
|
||||
<!-- <div class="row"> -->
|
||||
<!-- <h4 class="subtitle">Developers First</h4> -->
|
||||
<!-- <img class="code-example" src="img/code-placeholder.svg"> -->
|
||||
<!-- </div> -->
|
||||
<div>
|
||||
<img style="width: 200px" src="img/app-store-coming-soon.svg" />
|
||||
</div>
|
||||
</body>
|
||||
<div style="margin-top: 20px">
|
||||
<p>
|
||||
<a href="https://damus.io/log">The Damus Log</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://testflight.apple.com/join/CLwjLxWl">Join the TestFlight Beta</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<!-- daniel-testimonial -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
120
js/contacts.js
|
@ -1,120 +0,0 @@
|
|||
// TODO track friend sphere another way by using graph nodes
|
||||
|
||||
function contact_is_friend(contacts, pk) {
|
||||
return contacts.friends.has(pk)
|
||||
}
|
||||
|
||||
function contacts_process_event(contacts, our_pubkey, ev) {
|
||||
if (ev.pubkey !== our_pubkey)
|
||||
return;
|
||||
if (contacts.event && ev.created_at < contacts.event.created_at)
|
||||
return;
|
||||
contacts.event = ev
|
||||
contacts.friends = new Set();
|
||||
for (const tag of ev.tags) {
|
||||
if (tag.length > 1 && tag[0] === "p") {
|
||||
contacts.friends.add(tag[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* contacts_save commits the contacts data to storage.
|
||||
*/
|
||||
async function contacts_save(contacts) {
|
||||
function _contacts_save(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
let tx = db.transaction("friends", "readwrite");
|
||||
let store = tx.objectStore("friends");
|
||||
tx.oncomplete = (ev) => {
|
||||
db.close();
|
||||
resolve();
|
||||
log_debug("contacts saved successfully", contacts.friends.size);
|
||||
}
|
||||
tx.onerror = (ev) => {
|
||||
db.close();
|
||||
log_error(`tx errorCode: ${ev.request.errorCode}`);
|
||||
window.alert("An error occured saving contacts. Check console.");
|
||||
reject(ev);
|
||||
};
|
||||
|
||||
store.clear().onsuccess = () => {
|
||||
contacts.friends.forEach((pubkey) => {
|
||||
//log_debug("storing", pubkey);
|
||||
store.put({pubkey});
|
||||
});
|
||||
};
|
||||
}
|
||||
return dbcall(_contacts_save);
|
||||
}
|
||||
|
||||
async function contacts_load(model) {
|
||||
function _contacts_load(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
const tx = db.transaction("friends", "readonly");
|
||||
const store = tx.objectStore("friends");
|
||||
const cursor = store.openCursor();
|
||||
cursor.onsuccess = (ev)=> {
|
||||
var cursor = ev.target.result;
|
||||
if (cursor) {
|
||||
//log_debug("cursor val", cursor.value);
|
||||
model.contacts.friends.add(cursor.value.pubkey);
|
||||
cursor.continue();
|
||||
} else {
|
||||
db.close();
|
||||
resolve();
|
||||
log_debug(`contacts loaded successfully ${model.contacts.friends.size}`);
|
||||
}
|
||||
}
|
||||
cursor.onerror = (ev) => {
|
||||
db.close();
|
||||
reject(ev);
|
||||
log_error("Could not load contacts.");
|
||||
}
|
||||
}
|
||||
return dbcall(_contacts_load);
|
||||
}
|
||||
|
||||
// TODO move database methods to it's own file
|
||||
|
||||
async function dbcall(fn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var open = indexedDB.open("damus", 5);
|
||||
open.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
if (!db.objectStoreNames.contains("friends"))
|
||||
db.createObjectStore("friends", {keyPath: "pubkey"});
|
||||
if (!db.objectStoreNames.contains("events"))
|
||||
db.createObjectStore("events", {keyPath: "id"});
|
||||
if (!db.objectStoreNames.contains("settings"))
|
||||
db.createObjectStore("settings", {keyPath: "pubkey"});
|
||||
};
|
||||
open.onsuccess = (ev) => {
|
||||
fn(ev, resolve, reject);
|
||||
}
|
||||
open.onerror = (ev) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function dbclear() {
|
||||
function _dbclear(ev, resolve, reject) {
|
||||
const stores = ["friends", "events", "settings"];
|
||||
const db = ev.target.result;
|
||||
const tx = db.transaction(stores, "readwrite");
|
||||
tx.oncomplete = (ev) => {
|
||||
db.close();
|
||||
resolve();
|
||||
log_debug("cleared database");
|
||||
}
|
||||
tx.onerror = (ev) => {
|
||||
db.close();
|
||||
log_error(`tx errorCode: ${ev.request.errorCode}`);
|
||||
reject(ev);
|
||||
};
|
||||
for (const store of stores) {
|
||||
tx.objectStore(store).clear();
|
||||
}
|
||||
}
|
||||
return dbcall(_dbclear);
|
||||
}
|
294
js/core.js
|
@ -1,294 +0,0 @@
|
|||
const KIND_METADATA = 0;
|
||||
const KIND_NOTE = 1;
|
||||
const KIND_RELAY = 2;
|
||||
const KIND_CONTACT = 3;
|
||||
const KIND_DM = 4;
|
||||
const KIND_DELETE = 5;
|
||||
const KIND_SHARE = 6;
|
||||
const KIND_REACTION = 7;
|
||||
const KIND_CHATROOM = 42;
|
||||
|
||||
const TAG_P = "#p";
|
||||
const TAG_E = "#e";
|
||||
|
||||
const R_HEART = "❤️";
|
||||
const R_SHAKA = "🤙";
|
||||
|
||||
const STANDARD_KINDS = [
|
||||
KIND_NOTE,
|
||||
KIND_DM,
|
||||
KIND_DELETE,
|
||||
KIND_REACTION,
|
||||
KIND_SHARE,
|
||||
];
|
||||
const PUBLIC_KINDS = [
|
||||
KIND_NOTE,
|
||||
KIND_DELETE,
|
||||
KIND_REACTION,
|
||||
KIND_SHARE,
|
||||
];
|
||||
|
||||
async function broadcast_related_events(ev) {
|
||||
ev.tags.reduce((evs, tag) => {
|
||||
// cap it at something sane
|
||||
if (evs.length >= 5)
|
||||
return evs
|
||||
const ev = get_tag_event(tag)
|
||||
if (!ev)
|
||||
return evs
|
||||
return evs
|
||||
}, [])
|
||||
.forEach((ev, i) => {
|
||||
// so we don't get rate limited
|
||||
setTimeout(() => {
|
||||
log_debug("broadcasting related event", ev)
|
||||
broadcast_event(ev)
|
||||
}, (i+1)*1200)
|
||||
});
|
||||
}
|
||||
|
||||
function broadcast_event(ev) {
|
||||
DAMUS.pool.send(["EVENT", ev])
|
||||
}
|
||||
|
||||
async function share(evid) {
|
||||
const model = DAMUS;
|
||||
const e = model.all_events[evid];
|
||||
if (!e)
|
||||
return;
|
||||
let ev = {
|
||||
kind: KIND_SHARE,
|
||||
created_at: new_creation_time(),
|
||||
pubkey: model.pubkey,
|
||||
content: JSON.stringify(e),
|
||||
tags: [["e", e.id], ["p", e.pubkey]],
|
||||
}
|
||||
ev.id = await nostrjs.calculate_id(ev);
|
||||
ev = await sign_event(ev);
|
||||
broadcast_event(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
async function update_profile(profile={}) {
|
||||
let ev = {
|
||||
kind: KIND_METADATA,
|
||||
created_at: new_creation_time(),
|
||||
pubkey: DAMUS.pubkey,
|
||||
content: JSON.stringify(profile),
|
||||
tags: [],
|
||||
};
|
||||
ev.id = await nostrjs.calculate_id(ev);
|
||||
ev = await sign_event(ev);
|
||||
broadcast_event(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
async function update_contacts() {
|
||||
const model = DAMUS;
|
||||
const contacts = Array.from(model.contacts.friends);
|
||||
const tags = contacts.map((pubkey) => {
|
||||
return ["p", pubkey]
|
||||
});
|
||||
let ev = {
|
||||
kind: KIND_CONTACT,
|
||||
created_at: new_creation_time(),
|
||||
pubkey: model.pubkey,
|
||||
content: "",
|
||||
tags: tags,
|
||||
}
|
||||
ev.id = await nostrjs.calculate_id(ev);
|
||||
ev = await sign_event(ev);
|
||||
broadcast_event(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
async function sign_event(ev) {
|
||||
if (!(window.nostr && window.nostr.signEvent)) {
|
||||
console.error("window.nostr.signEvent is unsupported");
|
||||
return;
|
||||
}
|
||||
const signed = await window.nostr.signEvent(ev)
|
||||
if (typeof signed === 'string') {
|
||||
ev.sig = signed
|
||||
return ev
|
||||
}
|
||||
return signed
|
||||
}
|
||||
|
||||
function new_reply_tags(ev) {
|
||||
const tags = [["e", ev.id, "", "reply"]];
|
||||
if (ev.refs.root) {
|
||||
tags.push(["e", ev.refs.root, "", "root"]);
|
||||
}
|
||||
tags.push(["p", ev.pubkey]);
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function create_reply(pubkey, content, ev, all=true) {
|
||||
let kind = ev.kind;
|
||||
let tags = [];
|
||||
if (is_valid_reaction_content(content)) {
|
||||
// convert emoji replies into reactions
|
||||
kind = KIND_REACTION;
|
||||
tags.push(["e", ev.id], ["p", ev.pubkey]);
|
||||
} else {
|
||||
tags = all ? gather_reply_tags(pubkey, ev) : new_reply_tags(ev);
|
||||
}
|
||||
const created_at = new_creation_time();
|
||||
let reply = {
|
||||
pubkey,
|
||||
tags,
|
||||
content,
|
||||
created_at,
|
||||
kind
|
||||
};
|
||||
reply.id = await nostrjs.calculate_id(reply)
|
||||
reply = await sign_event(reply)
|
||||
return reply
|
||||
}
|
||||
|
||||
async function send_reply(content, replying_to, all=true) {
|
||||
const ev = DAMUS.all_events[replying_to]
|
||||
if (!ev)
|
||||
return;
|
||||
|
||||
const pubkey = await get_pubkey()
|
||||
let reply = await create_reply(pubkey, content, ev, all)
|
||||
|
||||
broadcast_event(reply)
|
||||
broadcast_related_events(reply)
|
||||
}
|
||||
|
||||
async function create_deletion_event(pubkey, target, content="") {
|
||||
const created_at = Math.floor(new Date().getTime() / 1000)
|
||||
let kind = 5
|
||||
|
||||
const tags = [["e", target]]
|
||||
let del = { pubkey, tags, content, created_at, kind }
|
||||
|
||||
del.id = await nostrjs.calculate_id(del)
|
||||
del = await sign_event(del)
|
||||
return del
|
||||
}
|
||||
|
||||
async function delete_post(id, reason) {
|
||||
const ev = DAMUS.all_events[id]
|
||||
if (!ev)
|
||||
return
|
||||
|
||||
const pubkey = await get_pubkey()
|
||||
let del = await create_deletion_event(pubkey, id, reason)
|
||||
broadcast_event(del)
|
||||
}
|
||||
|
||||
function model_get_reacts_to(model, pubkey, evid, emoji) {
|
||||
const r = model.reactions_to[evid];
|
||||
if (!r)
|
||||
return;
|
||||
for (const id of r.keys()) {
|
||||
if (model_is_event_deleted(model, id))
|
||||
continue;
|
||||
const reaction = model.all_events[id];
|
||||
if (!reaction || reaction.pubkey != pubkey)
|
||||
continue;
|
||||
if (emoji == get_reaction_emoji(reaction))
|
||||
return reaction;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function get_reactions(model, evid) {
|
||||
const reactions_set = model.reactions_to[evid]
|
||||
if (!reactions_set)
|
||||
return ""
|
||||
|
||||
let reactions = []
|
||||
for (const id of reactions_set.keys()) {
|
||||
if (model_is_event_deleted(model, id))
|
||||
continue
|
||||
const reaction = model.all_events[id]
|
||||
if (!reaction)
|
||||
continue
|
||||
reactions.push(reaction)
|
||||
}
|
||||
|
||||
const groups = reactions.reduce((grp, r) => {
|
||||
const e = get_reaction_emoji(r)
|
||||
grp[e] = grp[e] || {}
|
||||
grp[e][r.pubkey] = r
|
||||
return grp
|
||||
}, {})
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
function gather_reply_tags(pubkey, from) {
|
||||
let tags = []
|
||||
let ids = new Set()
|
||||
|
||||
if (from.refs && from.refs.root) {
|
||||
tags.push(["e", from.refs.root, "", "root"])
|
||||
ids.add(from.refs.root)
|
||||
}
|
||||
|
||||
tags.push(["e", from.id, "", "reply"])
|
||||
ids.add(from.id)
|
||||
|
||||
for (const tag of from.tags) {
|
||||
if (tag.length >= 2) {
|
||||
if (tag[0] === "p" && tag[1] !== pubkey) {
|
||||
if (!ids.has(tag[1])) {
|
||||
tags.push(["p", tag[1]])
|
||||
ids.add(tag[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (from.pubkey !== pubkey && !ids.has(from.pubkey)) {
|
||||
tags.push(["p", from.pubkey])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
function get_tag_event(tag) {
|
||||
const model = DAMUS;
|
||||
if (tag.length < 2)
|
||||
return null
|
||||
if (tag[0] === "e")
|
||||
return model.all_events[tag[1]]
|
||||
if (tag[0] === "p") {
|
||||
let profile = model_get_profile(model, tag[1]);
|
||||
if (profile.evid)
|
||||
return model.all_events[profile.evid];
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function* yield_etags(tags) {
|
||||
for (const tag of tags) {
|
||||
if (tag.length >= 2 && tag[0] === "e")
|
||||
yield tag
|
||||
}
|
||||
}
|
||||
|
||||
function get_content_warning(tags) {
|
||||
for (const tag of tags) {
|
||||
if (tag.length >= 1 && tag[0] === "content-warning")
|
||||
return tag[1] || ""
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function get_nip05_pubkey(email) {
|
||||
const [user, host] = email.split("@")
|
||||
const url = `https://${host}/.well-known/nostr.json?name=${user}`
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
const json = await res.json()
|
||||
log_debug("nip05 data", json)
|
||||
return json.names[user]
|
||||
} catch (e) {
|
||||
log_error("fetching nip05 entry for %s", email, e)
|
||||
throw e
|
||||
}
|
||||
}
|
41
js/lib.js
|
@ -1,41 +0,0 @@
|
|||
function load_our_relays(our_pubkey, pool, ev) {
|
||||
if (ev.pubkey != our_pubkey)
|
||||
return
|
||||
let relays
|
||||
try {
|
||||
relays = JSON.parse(ev.content)
|
||||
} catch (e) {
|
||||
log_error("error loading relays", e)
|
||||
return
|
||||
}
|
||||
for (const relay of Object.keys(relays)) {
|
||||
if (!pool.has(relay)) {
|
||||
log_debug("adding relay", relay)
|
||||
pool.add(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DEPRECATED */
|
||||
|
||||
function is_deleted(model, evid) {
|
||||
log_warn("is_deleted deprecated, use model_is_event_deleted");
|
||||
return model_is_event_deleted(model, evid);
|
||||
}
|
||||
function process_event(model, ev) {
|
||||
log_warn("process_event deprecated, use event_process");
|
||||
return model_process_event(model, ev);
|
||||
}
|
||||
function calculate_pow(ev) {
|
||||
log_warn("calculate_pow deprecated, use event_calculate_pow");
|
||||
return event_calculate_pow(ev);
|
||||
}
|
||||
function can_reply(ev) {
|
||||
log_warn("can_reply is deprecated, use event_can_reply");
|
||||
return event_can_reply(ev);
|
||||
}
|
||||
function passes_spam_filter(contacts, ev, pow) {
|
||||
log_warn("passes_spam_filter deprecated, use event_is_spam");
|
||||
return !event_is_spam(ev, contacts, pow);
|
||||
}
|
||||
|
391
js/lnsocket.js
Normal file
BIN
js/lnsocket.wasm
Executable file
316
js/main.js
|
@ -1,316 +0,0 @@
|
|||
let DAMUS = new_model();
|
||||
|
||||
// TODO autogenerate these constants with a bash script
|
||||
const IMG_EVENT_LIKED = "/icon/event-liked.svg";
|
||||
const IMG_EVENT_LIKE = "/icon/event-like.svg";
|
||||
const IMG_NO_USER = "/icon/no-user.svg";
|
||||
|
||||
const SID_META = "meta";
|
||||
const SID_HISTORY = "hist";
|
||||
const SID_NOTIFICATIONS = "noti";
|
||||
const SID_DMS_OUT = "dout";
|
||||
const SID_DMS_IN = "din";
|
||||
const SID_PROFILES = "prof";
|
||||
const SID_THREAD = "thrd";
|
||||
const SID_FRIENDS = "frds";
|
||||
const SID_EVENT = "evnt";
|
||||
|
||||
// This is our main entry.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
|
||||
addEventListener('DOMContentLoaded', (ev) => {
|
||||
damus_web_init();
|
||||
document.addEventListener("click", onclick_any);
|
||||
});
|
||||
|
||||
async function damus_web_init() {
|
||||
let tries = 0;
|
||||
const interval = 20;
|
||||
function init() {
|
||||
if (window.nostr) {
|
||||
log_info("init after", tries);
|
||||
damus_web_init_ready();
|
||||
return;
|
||||
}
|
||||
// TODO if tries is too many say window.nostr not found.
|
||||
tries++;
|
||||
setTimeout(init, interval);
|
||||
}
|
||||
init();
|
||||
}
|
||||
|
||||
async function damus_web_init_ready() {
|
||||
const model = DAMUS;
|
||||
model.pubkey = await get_pubkey(false);
|
||||
|
||||
find_node("#container-busy").classList.add("hide");
|
||||
if (!model.pubkey) {
|
||||
find_node("#container-welcome").classList.remove("hide");
|
||||
return;
|
||||
}
|
||||
find_node("#container-app").classList.remove("hide");
|
||||
webapp_init();
|
||||
}
|
||||
|
||||
async function signin() {
|
||||
const model = DAMUS;
|
||||
try {
|
||||
model.pubkey = await get_pubkey();
|
||||
} catch (err) {
|
||||
window.alert("An error occured trying to get your public key.");
|
||||
return;
|
||||
}
|
||||
if (!model.pubkey) {
|
||||
window.alert("No public key was aquired.");
|
||||
return;
|
||||
}
|
||||
find_node("#container-welcome").classList.add("hide");
|
||||
find_node("#container-app").classList.remove("hide");
|
||||
await webapp_init();
|
||||
}
|
||||
|
||||
async function webapp_init() {
|
||||
let err;
|
||||
const model = DAMUS;
|
||||
|
||||
// WARNING Order Matters!
|
||||
init_message_textareas();
|
||||
init_timeline(model);
|
||||
init_my_pfp(model);
|
||||
init_postbox(model);
|
||||
init_profile();
|
||||
view_show_spinner(true);
|
||||
|
||||
// Load data from storage
|
||||
await model_load_settings(model);
|
||||
init_settings(model);
|
||||
|
||||
// Create our pool so that event processing functions can work
|
||||
const pool = nostrjs.RelayPool(model.relays);
|
||||
model.pool = pool
|
||||
pool.on("open", on_pool_open);
|
||||
pool.on("event", on_pool_event);
|
||||
pool.on("notice", on_pool_notice);
|
||||
pool.on("eose", on_pool_eose);
|
||||
pool.on("ok", on_pool_ok);
|
||||
|
||||
var { mode, opts, valid } = parse_url_mode();
|
||||
view_timeline_apply_mode(model, mode, opts, !valid);
|
||||
on_timer_timestamps();
|
||||
on_timer_invalidations();
|
||||
on_timer_save();
|
||||
on_timer_tick();
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
function parse_url_mode() {
|
||||
var mode;
|
||||
var valid = true;
|
||||
var opts = {};
|
||||
var parts = window.location.pathname.split("/").slice(1);
|
||||
for (var key in VIEW_NAMES) {
|
||||
if (VIEW_NAMES[key].toLowerCase() == parts[0]) {
|
||||
mode = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mode) {
|
||||
mode = VM_FRIENDS;
|
||||
valid = false;
|
||||
}
|
||||
switch (mode) {
|
||||
case VM_FRIENDS:
|
||||
//opts.hide_replys = true;
|
||||
break;
|
||||
case VM_THREAD:
|
||||
opts.thread_id = parts[1];
|
||||
break;
|
||||
case VM_DM_THREAD:
|
||||
case VM_USER:
|
||||
opts.pubkey = parts[1];
|
||||
break;
|
||||
}
|
||||
return { mode, opts, valid };
|
||||
}
|
||||
|
||||
function on_timer_timestamps() {
|
||||
setTimeout(() => {
|
||||
view_timeline_update_timestamps();
|
||||
on_timer_timestamps();
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
function on_timer_invalidations() {
|
||||
const model = DAMUS;
|
||||
setTimeout(async () => {
|
||||
if (model.dms_need_redraw && view_get_timeline_el().dataset.mode == VM_DM) {
|
||||
// if needs decryption do it
|
||||
await decrypt_dms(model);
|
||||
view_dm_update(model);
|
||||
model.dms_need_redraw = false;
|
||||
view_show_spinner(false);
|
||||
}
|
||||
if (model.invalidated.length > 0)
|
||||
view_timeline_update(model);
|
||||
on_timer_invalidations();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function on_timer_save() {
|
||||
setTimeout(() => {
|
||||
const model = DAMUS;
|
||||
//model_save_events(model);
|
||||
model_save_settings(model);
|
||||
on_timer_save();
|
||||
}, 1 * 1000);
|
||||
}
|
||||
|
||||
function on_timer_tick() {
|
||||
const model = DAMUS;
|
||||
setTimeout(async () => {
|
||||
update_notifications(model);
|
||||
model.relay_que.forEach((que, relay) => {
|
||||
model_fetch_next_profile(model, relay);
|
||||
});
|
||||
on_timer_tick();
|
||||
}, 1 * 1000);
|
||||
}
|
||||
|
||||
/* on_pool_open occurs when a relay is opened. It then subscribes for the
|
||||
* relative REQ as needed.
|
||||
*/
|
||||
function on_pool_open(relay) {
|
||||
log_info(`OPEN(${relay.url})`);
|
||||
const model = DAMUS;
|
||||
const { pubkey } = model;
|
||||
|
||||
// Get all our info & history, well close this after we get it
|
||||
fetch_profile_info(pubkey, model.pool, relay);
|
||||
|
||||
// Get our notifications
|
||||
relay.subscribe(SID_NOTIFICATIONS, [{
|
||||
kinds: PUBLIC_KINDS,
|
||||
"#p": [pubkey],
|
||||
limit: 5000,
|
||||
}]);
|
||||
|
||||
// Get our dms. You have to do 2 separate queries: ours out and others in
|
||||
relay.subscribe(SID_DMS_IN, [{
|
||||
kinds: [KIND_DM],
|
||||
"#p": [pubkey],
|
||||
}]);
|
||||
relay.subscribe(SID_DMS_OUT, [{
|
||||
kinds: [KIND_DM],
|
||||
authors: [pubkey],
|
||||
}]);
|
||||
}
|
||||
|
||||
function on_pool_notice(relay, notice) {
|
||||
log_info(`NOTICE(${relay.url}): ${notice}`);
|
||||
}
|
||||
|
||||
// on_pool_eose occurs when all storage from a relay has been sent to the
|
||||
// client for a labeled (sub_id) REQ.
|
||||
async function on_pool_eose(relay, sub_id) {
|
||||
log_info(`EOSE(${relay.url}): ${sub_id}`);
|
||||
const model = DAMUS;
|
||||
const { pool } = model;
|
||||
const index = sub_id.indexOf(":");
|
||||
const sid = sub_id.slice(0, index >= 0 ? index : sub_id.length);
|
||||
const identifier = sub_id.slice(index+1);
|
||||
switch (sid) {
|
||||
case SID_HISTORY:
|
||||
case SID_THREAD:
|
||||
view_timeline_refresh(model);
|
||||
pool.unsubscribe(sub_id, relay);
|
||||
break
|
||||
case SID_FRIENDS:
|
||||
view_timeline_refresh(model);
|
||||
break
|
||||
case SID_META:
|
||||
if (model.pubkey == identifier) {
|
||||
friends = Array.from(model.contacts.friends);
|
||||
friends.push(identifier);
|
||||
fetch_friends_history(friends, pool, relay);
|
||||
log_debug("Got our friends after no init & fetching our friends");
|
||||
}
|
||||
case SID_NOTIFICATIONS:
|
||||
case SID_PROFILES:
|
||||
case SID_EVENT:
|
||||
pool.unsubscribe(sub_id, relay);
|
||||
break;
|
||||
case SID_DMS_OUT:
|
||||
case SID_DMS_IN:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function on_pool_event(relay, sub_id, ev) {
|
||||
const model = DAMUS;
|
||||
|
||||
// Simply ignore any events that happened in the future.
|
||||
if (new Date(ev.created_at * 1000) > new Date()) {
|
||||
log_debug(`blocked event caust it was newer`, ev);
|
||||
return;
|
||||
}
|
||||
model_process_event(model, relay, ev);
|
||||
}
|
||||
|
||||
function on_pool_ok(relay, evid, status) {
|
||||
log_debug(`OK(${relay.url}): ${evid} = '${status}'`);
|
||||
}
|
||||
|
||||
function fetch_profiles(pool, relay, pubkeys) {
|
||||
log_debug(`(${relay.url}) fetching '${pubkeys.length} profiles'`);
|
||||
pool.subscribe(SID_PROFILES, [{
|
||||
kinds: [KIND_METADATA],
|
||||
authors: pubkeys,
|
||||
}], relay);
|
||||
}
|
||||
|
||||
function fetch_profile_info(pubkey, pool, relay) {
|
||||
const sid = `${SID_META}:${pubkey}`;
|
||||
pool.subscribe(sid, [{
|
||||
kinds: [KIND_METADATA, KIND_CONTACT, KIND_RELAY],
|
||||
authors: [pubkey],
|
||||
}], relay);
|
||||
return sid;
|
||||
}
|
||||
|
||||
function fetch_profile(pubkey, pool, relay) {
|
||||
fetch_profile_info(pubkey, pool, relay);
|
||||
pool.subscribe(`${SID_HISTORY}:${pubkey}`, [{
|
||||
kinds: PUBLIC_KINDS,
|
||||
authors: [pubkey],
|
||||
limit: 1000,
|
||||
}], relay);
|
||||
}
|
||||
|
||||
function fetch_event(evid, pool) {
|
||||
const sid = `${SID_EVENT}:${evid}`;
|
||||
pool.subscribe(sid, [{
|
||||
ids: [evid]
|
||||
}]);
|
||||
log_debug(`fetching event ${sid}`);
|
||||
}
|
||||
|
||||
function fetch_thread_history(evid, pool) {
|
||||
// TODO look up referenced relays for thread history
|
||||
fetch_event(evid, pool);
|
||||
const sid = `${SID_THREAD}:${evid}`
|
||||
pool.subscribe(sid, [{
|
||||
kinds: PUBLIC_KINDS,
|
||||
"#e": [evid],
|
||||
}]);
|
||||
log_debug(`fetching thread ${sid}`);
|
||||
}
|
||||
|
||||
function fetch_friends_history(friends, pool, relay) {
|
||||
// TODO fetch history of each friend by their desired relay
|
||||
pool.subscribe(SID_FRIENDS, [{
|
||||
kinds: PUBLIC_KINDS,
|
||||
authors: friends,
|
||||
limit: 5000,
|
||||
}], relay);
|
||||
log_debug(`fetching friends history`);
|
||||
}
|
512
js/model.js
|
@ -1,512 +0,0 @@
|
|||
/* model_process_event is the main point where events are post-processed from
|
||||
* a relay. Additionally other side effects happen such as notification checks
|
||||
* and fetching of unknown pubkey profiles.
|
||||
*/
|
||||
function model_process_event(model, relay, ev) {
|
||||
if (model.all_events[ev.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fetch_profile = false;
|
||||
model.all_events[ev.id] = ev;
|
||||
ev.refs = event_get_tag_refs(ev.tags);
|
||||
ev.pow = event_calculate_pow(ev);
|
||||
|
||||
// Process specific event needs based on it's kind. Not using a map because
|
||||
// integers can't be used.
|
||||
let fn;
|
||||
switch(ev.kind) {
|
||||
case KIND_NOTE:
|
||||
case KIND_SHARE:
|
||||
fetch_profile = true;
|
||||
break;
|
||||
case KIND_METADATA:
|
||||
fn = model_process_event_metadata;
|
||||
break;
|
||||
case KIND_CONTACT:
|
||||
fn = model_process_event_following;
|
||||
break;
|
||||
case KIND_DELETE:
|
||||
fn = model_process_event_deletion;
|
||||
break;
|
||||
case KIND_REACTION:
|
||||
fn = model_process_event_reaction;
|
||||
break;
|
||||
case KIND_DM:
|
||||
fetch_profile = true;
|
||||
fn = model_process_event_dm;
|
||||
break;
|
||||
}
|
||||
if (fn)
|
||||
fn(model, ev, !!relay);
|
||||
|
||||
// Queue event for rendering
|
||||
model.invalidated.push(ev.id);
|
||||
|
||||
// If the processing did not come from a relay, but instead storage then
|
||||
// let us simply ignore fetching new things.
|
||||
if (!relay)
|
||||
return;
|
||||
|
||||
// Request new profiles for unseen pubkeys of the event
|
||||
if (fetch_profile) {
|
||||
event_get_pubkeys(ev).forEach((pubkey) => {
|
||||
if (!model_has_profile(model, pubkey)) {
|
||||
model_que_profile(model, relay, pubkey);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function model_get_relay_que(model, relay) {
|
||||
return map_get(model.relay_que, relay, {
|
||||
profiles: [],
|
||||
timestamp: 0,
|
||||
contacts_init: false,
|
||||
});
|
||||
}
|
||||
|
||||
function model_que_profile(model, relay, pubkey) {
|
||||
const que = model_get_relay_que(model, relay);
|
||||
if (que.profiles.indexOf(pubkey) >= 0)
|
||||
return;
|
||||
que.profiles.push(pubkey);
|
||||
}
|
||||
|
||||
function model_clear_profile_que(model, relay, sid) {
|
||||
const que = model_get_relay_que(model, relay);
|
||||
//log_debug(`cmp '${que.sid}' vs '${sid}'`);
|
||||
if (que.sid != sid)
|
||||
return;
|
||||
delete que.current;
|
||||
delete que.sid;
|
||||
que.timestamp = 0;
|
||||
log_debug("cleared qued");
|
||||
}
|
||||
|
||||
function model_fetch_next_profile(model, relay) {
|
||||
const que = model_get_relay_que(model, relay);
|
||||
|
||||
// Give up on existing case and add it back to the que
|
||||
if (que.current) {
|
||||
if ((new Date().getTime() - que.timestamp) / 1000 > 30) {
|
||||
que.profiles = que.profiles.concat(que.current);
|
||||
model.pool.unsubscribe(SID_PROFILES, relay);
|
||||
log_debug(`(${relay.url}) gave up on ${que.current.length} profiles`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (que.profiles.length == 0) {
|
||||
delete que.current;
|
||||
return;
|
||||
}
|
||||
log_debug(`(${relay.url}) has '${que.profiles.length} left profiles to fetch'`);
|
||||
|
||||
const set = new Set();
|
||||
let i = 0;
|
||||
while (que.profiles.length > 0 && i < 100) {
|
||||
set.add(que.profiles.shift());
|
||||
i++;
|
||||
}
|
||||
que.timestamp = new Date().getTime();
|
||||
que.current = Array.from(set);
|
||||
fetch_profiles(model.pool, relay, que.current);
|
||||
}
|
||||
|
||||
/* model_process_event_profile updates the matching profile with the contents found
|
||||
* in the event.
|
||||
*/
|
||||
function model_process_event_metadata(model, ev, update_view) {
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const evs = model.all_events;
|
||||
if (profile.evid &&
|
||||
evs[ev.id].created_at < evs[profile.evid].created_at)
|
||||
return;
|
||||
profile.evid = ev.id;
|
||||
profile.data = safe_parse_json(ev.content, "profile contents");
|
||||
if (update_view)
|
||||
view_timeline_update_profiles(model, profile.pubkey);
|
||||
// If it's my pubkey let's redraw my pfp that is not located in the view
|
||||
// This has to happen regardless of update_view because of the it's not
|
||||
// related to events
|
||||
/*if (profile.pubkey == model.pubkey) {
|
||||
redraw_my_pfp(model);
|
||||
}*/
|
||||
}
|
||||
|
||||
function model_has_profile(model, pk) {
|
||||
return !!model_get_profile(model, pk).evid;
|
||||
}
|
||||
|
||||
function model_get_profile(model, pubkey) {
|
||||
if (model.profiles.has(pubkey)) {
|
||||
return model.profiles.get(pubkey);
|
||||
}
|
||||
model.profiles.set(pubkey, {
|
||||
pubkey: pubkey,
|
||||
evid: "",
|
||||
relays: [],
|
||||
data: {},
|
||||
});
|
||||
return model.profiles.get(pubkey);
|
||||
}
|
||||
|
||||
function model_process_event_following(model, ev, update_view) {
|
||||
contacts_process_event(model.contacts, model.pubkey, ev)
|
||||
// TODO support loading relays that are stored on the initial relay
|
||||
// I find this wierd since I may never want to do that and only have that
|
||||
// information provided by the client - to be better understood
|
||||
// load_our_relays(model.pubkey, model.pool, ev)
|
||||
}
|
||||
|
||||
/* model_process_event_dm updates the internal dms hash map based on dms
|
||||
* targeted at the user.
|
||||
*/
|
||||
function model_process_event_dm(model, ev, update_view) {
|
||||
if (!event_is_dm(ev, model.pubkey))
|
||||
return;
|
||||
// We have to identify who the target DM is for since we are also in the
|
||||
// chat. We simply use the first non-us key we find as the target. I am not
|
||||
// sure that multi-sig chats are possible at this time in the spec. If no
|
||||
// target, it's a bad DM.
|
||||
let target;
|
||||
const keys = event_get_pubkeys(ev);
|
||||
for (let key of keys) {
|
||||
target = key;
|
||||
if (key == model.pubkey)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (!target)
|
||||
return;
|
||||
let dm = model_get_dm(model, target);
|
||||
dm.needs_decryption = true;
|
||||
dm.needs_redraw = true;
|
||||
// It may be faster to not use binary search due to the newest always being
|
||||
// at the front - but I could be totally wrong. Potentially it COULD be
|
||||
// slower during history if history is imported ASCENDINGLY. But anything
|
||||
// after this will always be faster and is insurance (race conditions).
|
||||
let i = 0;
|
||||
for (; i < dm.events.length; i++) {
|
||||
const b = dm.events[i];
|
||||
if (ev.created_at > b.created_at)
|
||||
break;
|
||||
}
|
||||
dm.events.splice(i, 0, ev);
|
||||
|
||||
// Check if DM is new
|
||||
if (ev.created_at > dm.last_viewed) {
|
||||
dm.new_count++;
|
||||
// dirty hack but works well
|
||||
model.dms_need_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
function model_get_dm(model, target) {
|
||||
if (!model.dms.has(target)) {
|
||||
// TODO think about using "pubkey:subject" so we have threads
|
||||
model.dms.set(target, {
|
||||
pubkey: target,
|
||||
// events is an ordered list (new to old) of events referenced from
|
||||
// all_events. It should not be a copy to reduce memory.
|
||||
events: [],
|
||||
// Last read event time by the client/user
|
||||
last_viewed: 0,
|
||||
new_count: 0,
|
||||
// Notifies the renderer that this dm is out of date
|
||||
needs_redraw: false,
|
||||
needs_decryption: false,
|
||||
});
|
||||
}
|
||||
return model.dms.get(target);
|
||||
}
|
||||
|
||||
function model_get_dms_seen(model) {
|
||||
const obj = {};
|
||||
for (let item of model.dms) {
|
||||
const dm = item[1];
|
||||
obj[dm.pubkey] = dm.last_viewed;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function model_set_dms_seen(model, obj={}) {
|
||||
for (const pubkey in obj) {
|
||||
model_get_dm(model, pubkey).last_viewed = obj[pubkey];
|
||||
}
|
||||
}
|
||||
|
||||
function model_dm_seen(model, target) {
|
||||
const dm = model_get_dm(model, target);
|
||||
if (!dm.events[0])
|
||||
return;
|
||||
dm.last_viewed = dm.events[0].created_at;
|
||||
dm.new_count = 0;
|
||||
dm.needs_redraw = true;
|
||||
}
|
||||
|
||||
function model_mark_dms_seen(model) {
|
||||
model.dms.forEach((dm) => {
|
||||
model_dm_seen(model, dm.pubkey);
|
||||
});
|
||||
model.dms_need_redraw = true;
|
||||
}
|
||||
|
||||
/* model_process_event_reaction updates the reactions dictionary
|
||||
*/
|
||||
function model_process_event_reaction(model, ev, update_view) {
|
||||
let reaction = event_parse_reaction(ev);
|
||||
if (!reaction) {
|
||||
return;
|
||||
}
|
||||
if (!model.reactions_to[reaction.e])
|
||||
model.reactions_to[reaction.e] = new Set();
|
||||
model.reactions_to[reaction.e].add(ev.id);
|
||||
if (update_view)
|
||||
view_timeline_update_reaction(model, ev);
|
||||
}
|
||||
|
||||
/* event_process_deletion updates the list of deleted events. Additionally
|
||||
* pushes event ids onto the invalidated stack for any found.
|
||||
*/
|
||||
function model_process_event_deletion(model, ev, update_view) {
|
||||
for (const tag of ev.tags) {
|
||||
if (tag.length >= 2 && tag[0] === "e" && tag[1]) {
|
||||
let evid = tag[1];
|
||||
model.invalidated.push(evid);
|
||||
model_remove_reaction(model, evid, update_view);
|
||||
if (model.deleted[evid])
|
||||
continue;
|
||||
let ds = model.deletions[evid] =
|
||||
(model.deletions[evid] || new Set());
|
||||
ds.add(ev.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function model_remove_reaction(model, evid, update_view) {
|
||||
// deleted_ev -> target_ev -> original_ev
|
||||
// Here we want to find the original react event to and remove it from our
|
||||
// reactions map, then we want to update the element on the page. If the
|
||||
// server does not clean up events correctly the increment/decrement method
|
||||
// should work fine in theory.
|
||||
const target_ev = model.all_events[evid];
|
||||
if (!target_ev)
|
||||
return;
|
||||
const reaction = event_parse_reaction(target_ev);
|
||||
if (!reaction)
|
||||
return;
|
||||
if (model.reactions_to[reaction.e])
|
||||
model.reactions_to[reaction.e].delete(target_ev.id);
|
||||
if (update_view)
|
||||
view_timeline_update_reaction(model, target_ev);
|
||||
}
|
||||
|
||||
function model_is_event_deleted(model, evid) {
|
||||
// we've already know it's deleted
|
||||
if (model.deleted[evid])
|
||||
return model.deleted[evid]
|
||||
|
||||
const ev = model.all_events[evid]
|
||||
if (!ev)
|
||||
return false
|
||||
|
||||
// all deletion events
|
||||
const ds = model.deletions[ev.id]
|
||||
if (!ds)
|
||||
return false
|
||||
|
||||
// find valid deletion events
|
||||
for (const id of ds.keys()) {
|
||||
const d_ev = model.all_events[id]
|
||||
if (!d_ev)
|
||||
continue
|
||||
|
||||
// only allow deletes from the user who created it
|
||||
if (d_ev.pubkey === ev.pubkey) {
|
||||
model.deleted[ev.id] = d_ev
|
||||
delete model.deletions[ev.id]
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function model_has_event(model, evid) {
|
||||
return evid in model.all_events
|
||||
}
|
||||
|
||||
function model_events_arr(model) {
|
||||
const events = model.all_events;
|
||||
let arr = [];
|
||||
for (const evid in events) {
|
||||
const ev = events[evid];
|
||||
const i = arr_bsearch_insert(arr, ev, event_cmp_created);
|
||||
arr.splice(i, 0, ev);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function test_model_events_arr() {
|
||||
const arr = model_events_arr({all_events: {
|
||||
"c": {name: "c", created_at: 2},
|
||||
"a": {name: "a", created_at: 0},
|
||||
"b": {name: "b", created_at: 1},
|
||||
"e": {name: "e", created_at: 4},
|
||||
"d": {name: "d", created_at: 3},
|
||||
}});
|
||||
let last;
|
||||
while(arr.length > 0) {
|
||||
let ev = arr.pop();
|
||||
log_debug("test:", ev.name, ev.created_at);
|
||||
if (!last) {
|
||||
last = ev;
|
||||
continue;
|
||||
}
|
||||
if (ev.created_at > last.created_at) {
|
||||
log_error(`ev ${ev.name} should be before ${last.name}`);
|
||||
}
|
||||
last = ev;
|
||||
}
|
||||
}
|
||||
|
||||
async function model_save_settings(model) {
|
||||
function _settings_save(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
const tx = db.transaction("settings", "readwrite");
|
||||
const store = tx.objectStore("settings");
|
||||
tx.oncomplete = (ev) => {
|
||||
db.close();
|
||||
resolve();
|
||||
//log_debug("settings saved");
|
||||
};
|
||||
tx.onerror = (ev) => {
|
||||
db.close();
|
||||
log_error("failed to save events");
|
||||
reject(ev);
|
||||
};
|
||||
store.clear().onsuccess = () => {
|
||||
store.put({
|
||||
pubkey: model.pubkey,
|
||||
notifications_last_viewed: model.notifications.last_viewed,
|
||||
relays: Array.from(model.relays),
|
||||
dms_seen: model_get_dms_seen(model),
|
||||
});
|
||||
};
|
||||
}
|
||||
return dbcall(_settings_save);
|
||||
}
|
||||
|
||||
async function model_load_settings(model) {
|
||||
function _settings_load(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
const tx = db.transaction("settings", "readonly");
|
||||
const store = tx.objectStore("settings");
|
||||
const req = store.get(model.pubkey);
|
||||
req.onsuccess = (ev) => {
|
||||
const settings = ev.target.result;
|
||||
if (settings) {
|
||||
model.notifications.last_viewed = settings.notifications_last_viewed;
|
||||
if (settings.relays.length)
|
||||
model.relays = new Set(settings.relays);
|
||||
model.embeds = settings.embeds ? settings.embeds : "friends";
|
||||
model_set_dms_seen(model, settings.dms_seen);
|
||||
}
|
||||
db.close();
|
||||
resolve();
|
||||
log_debug("Successfully loaded events");
|
||||
}
|
||||
req.onerror = (ev) => {
|
||||
db.close();
|
||||
reject(ev);
|
||||
log_error("Could not load settings.");
|
||||
};
|
||||
}
|
||||
return dbcall(_settings_load);
|
||||
}
|
||||
|
||||
async function model_save_events(model) {
|
||||
function _events_save(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
let tx = db.transaction("events", "readwrite");
|
||||
let store = tx.objectStore("events");
|
||||
tx.oncomplete = (ev) => {
|
||||
db.close();
|
||||
resolve();
|
||||
//log_debug("saved events!");
|
||||
};
|
||||
tx.onerror = (ev) => {
|
||||
db.close();
|
||||
log_error("failed to save events");
|
||||
reject(ev);
|
||||
};
|
||||
store.clear().onsuccess = ()=> {
|
||||
for (const evid in model.all_events) {
|
||||
store.put(model.all_events[evid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dbcall(_events_save);
|
||||
}
|
||||
|
||||
async function model_load_events(model, fn) {
|
||||
function _events_load(ev, resolve, reject) {
|
||||
const db = ev.target.result;
|
||||
const tx = db.transaction("events", "readonly");
|
||||
const store = tx.objectStore("events");
|
||||
const cursor = store.openCursor();
|
||||
cursor.onsuccess = (ev) => {
|
||||
var cursor = ev.target.result;
|
||||
if (cursor) {
|
||||
fn(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
db.close();
|
||||
resolve();
|
||||
log_debug("Successfully loaded events");
|
||||
}
|
||||
}
|
||||
cursor.onerror = (ev) => {
|
||||
db.close();
|
||||
reject(ev);
|
||||
log_error("Could not load events.");
|
||||
};
|
||||
}
|
||||
return dbcall(_events_load);
|
||||
}
|
||||
|
||||
function new_model() {
|
||||
return {
|
||||
all_events: {}, // our master list of all events
|
||||
notifications: {
|
||||
last_viewed: 0, // time since last looking at notifications
|
||||
count: 0, // the number not seen since last looking
|
||||
},
|
||||
profiles: new Map(), // pubkey => profile data
|
||||
contacts: {
|
||||
event: null,
|
||||
friends: new Set(),
|
||||
friend_of_friends: new Set(),
|
||||
},
|
||||
dms: new Map(), // pubkey => event list
|
||||
invalidated: [], // event ids which should be added/removed
|
||||
elements: {}, // map of evid > rendered element
|
||||
relay_que: new Map(),
|
||||
relays: new Set([
|
||||
"wss://nostr.oxtr.dev",
|
||||
"wss://relay.damus.io",
|
||||
"wss://no.str.cr",
|
||||
"wss://relay.snort.social",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.lucentlabs.co",
|
||||
]),
|
||||
|
||||
max_depth: 2,
|
||||
reactions_to: {},
|
||||
deletions: {},
|
||||
deleted: {},
|
||||
pow: 0, // pow difficulty target
|
||||
};
|
||||
}
|
1
js/qrcode.min.js
vendored
Normal file
172
js/tipjar.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
async function make_request(method, rune, params) {
|
||||
const LNSocket = await lnsocket_init()
|
||||
const ln = LNSocket()
|
||||
|
||||
ln.genkey()
|
||||
await ln.connect_and_init("03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71", "wss://cln.jb55.com:443")
|
||||
|
||||
const {result} = await ln.rpc({ rune, method, params })
|
||||
|
||||
ln.disconnect()
|
||||
return result
|
||||
}
|
||||
|
||||
function fetch_tipjar_summary() {
|
||||
const rune = "5sgpXcVRMy19h2Ai9LiklJ7jI_J3qNnnG36wvyViqR49OTQmbWV0aG9kPW9mZmVyLXN1bW1hcnkmcG5hbWVkZXNjcmlwdGlvbj1AZGFtdXMtYW5kcm9pZCZwbmFtZWxpbWl0PTU="
|
||||
return make_request("offer-summary", rune, {
|
||||
offerid: "2043536dfec68d559102f73510927622812a230cfdda079e96fccbfe35a96d11",
|
||||
description: "@damus-android",
|
||||
limit: 5
|
||||
})
|
||||
}
|
||||
|
||||
function make_invoice(description) {
|
||||
const rune = "LZwGZJO7wZgmoScFQb5reZ0Ii8qPKCeUfTb-UcbDxWw9MTImbWV0aG9kPWludm9pY2U="
|
||||
description = (description && `${description} @damus-android`) || "@damus-android donation"
|
||||
return make_request("invoice", rune, {
|
||||
amount_msat: "any",
|
||||
label: `damus-android-${new Date().getTime()}`,
|
||||
description: description
|
||||
})
|
||||
}
|
||||
|
||||
function make_qrcode(dat) {
|
||||
const link = dat.toUpperCase()
|
||||
document.querySelector("#qrcode").innerHTML = ""
|
||||
const qr = new QRCode("qrcode", {
|
||||
text: link,
|
||||
width: 256,
|
||||
height: 256,
|
||||
colorDark : "#000000",
|
||||
colorLight : "#ffffff",
|
||||
correctLevel : QRCode.CorrectLevel.L
|
||||
})
|
||||
}
|
||||
|
||||
async function click_make_invoice(el) {
|
||||
const offerdata = document.querySelector("#offerdata")
|
||||
const tipjar_img = document.querySelector("#tipjar-offer-qr")
|
||||
|
||||
const note = prompt("Leave a note!", "")
|
||||
|
||||
const invoice = await make_invoice(note)
|
||||
|
||||
make_qrcode("LIGHTNING:" + invoice.bolt11)
|
||||
document.querySelector("#bolt12").href = "lightning:" + invoice.bolt11
|
||||
|
||||
el.style.display = "none";
|
||||
}
|
||||
|
||||
async function copy_tip() {
|
||||
const offer = document.querySelector("#offerdata").value.trim();
|
||||
try {
|
||||
await navigator.clipboard.writeText(offer)
|
||||
alert("Invoice copied to clipboard!")
|
||||
} catch(err) {
|
||||
console.log("clipboard copy error", err)
|
||||
document.querySelector("#offerdata").style.display = "block"
|
||||
}
|
||||
}
|
||||
|
||||
async function go() {
|
||||
const summary = await fetch_tipjar_summary()
|
||||
|
||||
const el = document.querySelector("#tipjar-summary")
|
||||
const bolt12 = document.querySelector("#bolt12")
|
||||
|
||||
make_qrcode(bolt12.href)
|
||||
el.innerHTML = render_tips(summary)
|
||||
}
|
||||
|
||||
function render_tips(res) {
|
||||
const total_sats = res.total_msatoshi / 1000
|
||||
const goal = 3000000
|
||||
const perc = `${((total_sats / goal) * 100).toPrecision(2)}%`
|
||||
const total_fmt = `${format_amount(total_sats)} / ${format_amount(goal)} sats goal (${perc})`
|
||||
return `
|
||||
<p>Total: <b>${total_fmt}</b></p>
|
||||
<div class="progres" style="height:20px; background-color: #f1f1f1">
|
||||
<div class="progress-bar" style="background-color: #f44336; height: 100%;width: ${perc}"></div>
|
||||
</div>
|
||||
<h5>Recent Donors</h5>
|
||||
${render_table(res.paid_invoices)}
|
||||
<h5>Top Donors</h5>
|
||||
${render_table(res.top_donors)}
|
||||
`
|
||||
}
|
||||
|
||||
function render_table(invoices)
|
||||
{
|
||||
return `
|
||||
<table style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Note</th>
|
||||
<th>Amount</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${invoices.map(render_tip).join("\n")}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
function format_amount(amt)
|
||||
{
|
||||
return amt.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function render_tip(tip)
|
||||
{
|
||||
let note = tip.payer_note ? tip.payer_note : (tip.description || "Anonymous")
|
||||
note = note.replace("@damus-android", "")
|
||||
const amount = format_amount(tip.amount_msat / 1000)
|
||||
const now = Math.floor(new Date().getTime() / 1000)
|
||||
const date = time_delta(now * 1000, tip.paid_at * 1000)
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${note}</td>
|
||||
<td>${amount} sats</td>
|
||||
<td class="reldate">${date}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
function time_delta(current, previous) {
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
|
||||
var elapsed = current - previous;
|
||||
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed/1000) + ' seconds ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed/msPerMinute) + ' minutes ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerDay ) {
|
||||
return Math.round(elapsed/msPerHour ) + ' hours ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed/msPerDay) + ' days ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed/msPerMonth) + ' months ago';
|
||||
}
|
||||
|
||||
else {
|
||||
return Math.round(elapsed/msPerYear ) + ' years ago';
|
||||
}
|
||||
}
|
||||
|
||||
go()
|
85
js/ui/dm.js
|
@ -1,85 +0,0 @@
|
|||
function view_dm_update(model) {
|
||||
const el = find_node("#dms");
|
||||
const order = [];
|
||||
let reorder = false;
|
||||
model.dms.forEach((dm, pubkey, m) => {
|
||||
if (!dm.events.length)
|
||||
return;
|
||||
const i = arr_bsearch_insert(order, dm, dm_cmp);
|
||||
order.splice(i, 0, dm);
|
||||
if (!dm.needs_redraw)
|
||||
return;
|
||||
let gel = find_node(`[data-pubkey='${pubkey}']`, el);
|
||||
if (!gel) {
|
||||
gel = new_el_dmgroup(model, dm);
|
||||
gel.addEventListener("click", onclick_dm);
|
||||
find_node("img.pfp", gel).addEventListener("error", onerror_pfp);
|
||||
el.appendChild(gel);
|
||||
}
|
||||
update_el_dmgroup(model, dm, gel);
|
||||
dm.needs_redraw = false;
|
||||
reorder = true;
|
||||
});
|
||||
|
||||
// I'm not sure what is faster, doing a frag update all at once OR just
|
||||
// updating individual positions. If they all update it's slower, but the
|
||||
// chances of them all updating is is small and only garuenteed when it
|
||||
// draws the first time.
|
||||
//const frag = new DocumentFragment();
|
||||
if (!reorder) return;
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
let dm = order[i];
|
||||
let xel = el.children[i];
|
||||
if (dm.pubkey == xel.dataset.pubkey)
|
||||
continue;
|
||||
let gel = find_node(`[data-pubkey='${order[i].pubkey}']`, el);
|
||||
el.insertBefore(gel, xel);
|
||||
//frag.appendChild(gel);
|
||||
}
|
||||
//el.appendChild(frag);
|
||||
}
|
||||
|
||||
function dm_cmp(a, b) {
|
||||
const x = a.events[0].created_at;
|
||||
const y = b.events[0].created_at;
|
||||
if (x > y)
|
||||
return -1;
|
||||
if (x < y)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function update_el_dmgroup(model, dm, el) {
|
||||
const ev = dm.events[0];
|
||||
const profile = model_get_profile(model, dm.pubkey);
|
||||
const message = ev.decrypted || ev.content || "No Message.";
|
||||
const time = fmt_datetime(new Date(ev.created_at * 1000));
|
||||
const cel = find_node(".count", el)
|
||||
cel.innerText = dm.new_count || dm.events.length;
|
||||
cel.classList.toggle("active", dm.new_count > 0);
|
||||
find_node(".time", el).innerText = time;
|
||||
find_node(".message", el).innerText = message;
|
||||
find_node(".username", el).innerText = fmt_name(profile);
|
||||
}
|
||||
|
||||
function new_el_dmgroup(model, dm) {
|
||||
const profile = model_get_profile(model, dm.pubkey);
|
||||
return html2el(`<div class="dm-group bottom-border clickable" data-pubkey="${dm.pubkey}">
|
||||
<div>${render_profile_img(profile, true)}</div>
|
||||
<div class="content">
|
||||
<div class="count"></div>
|
||||
<label class="username" data-pubkey="${dm.pubkey}"></label>
|
||||
<p class="message"></p>
|
||||
<label class="time"></label>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function onclick_dm(ev) {
|
||||
const el = find_parent(ev.target, "[data-pubkey]");
|
||||
if (!el || !el.dataset.pubkey) {
|
||||
log_error("did not find dm pubkey");
|
||||
return;
|
||||
}
|
||||
view_timeline_apply_mode(DAMUS, VM_DM_THREAD, {pubkey: el.dataset.pubkey});
|
||||
}
|
126
js/ui/fmt.js
|
@ -1,126 +0,0 @@
|
|||
function linkify(text="") {
|
||||
return text.replace(URL_REGEX, function(match, p1, p2, p3) {
|
||||
const url = p2+p3;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch (err) {
|
||||
return match;
|
||||
}
|
||||
let markup = html`<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`;
|
||||
if (is_img_url(parsed.pathname)) {
|
||||
//markup += `<button action="open-attachments">Open Img</button>`;
|
||||
} else if (is_video_url(parsed.pathname)) {
|
||||
//markup += `<button action="open-attachments">Open Video</button>`;
|
||||
}
|
||||
return p1+markup;
|
||||
})
|
||||
}
|
||||
|
||||
function format_content(model, ev) {
|
||||
if (ev.kind === KIND_REACTION) {
|
||||
if (ev.content === "" || ev.content === "+")
|
||||
return "❤️"
|
||||
return html`${ev.content.trim()}`;
|
||||
}
|
||||
const content = (ev.kind == KIND_DM ? ev.decrypted || ev.content : ev.content)
|
||||
.trim();
|
||||
const body = fmt_mentions(model, fmt_body(content), ev.tags);
|
||||
let cw = get_content_warning(ev.tags)
|
||||
if (cw !== null) {
|
||||
let cwHTML = "Content Warning"
|
||||
if (cw === "") {
|
||||
cwHTML += "."
|
||||
} else {
|
||||
cwHTML += html`: "<span>${cw}</span>".`
|
||||
}
|
||||
return `
|
||||
<details class="cw">
|
||||
<summary class="event-message">${cwHTML}</summary>
|
||||
${body}
|
||||
</details>`;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function fmt_mentions(model, str, tags) {
|
||||
var buf = "";
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
let c = str.charAt(i);
|
||||
let peek = str.charAt(i+1);
|
||||
if (!(c == "#" && peek == "[")) {
|
||||
buf += c;
|
||||
continue;
|
||||
}
|
||||
const start = i;
|
||||
i += 2;
|
||||
let x = "";
|
||||
for(;;) {
|
||||
c = str.charAt(i);
|
||||
if (c >= '0' && c <= '9') {
|
||||
x += c;
|
||||
i++;
|
||||
} else if (c == ']') {
|
||||
break;
|
||||
} else {
|
||||
buf += x;
|
||||
x = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (x == "")
|
||||
continue;
|
||||
let tag = tags[parseInt(x)];
|
||||
if (!tag) {
|
||||
buf += `#[${x}]`
|
||||
continue;
|
||||
}
|
||||
let ref = tag[1];
|
||||
if (tag[0] == 'e') {
|
||||
buf += `<span class="clickable bold" action="open-thread"
|
||||
data-thread-id="${ref}">note:${fmt_pubkey(ref)}</span>`;
|
||||
} else if (tag[0] == 'p') {
|
||||
let profile = model_get_profile(model, ref);
|
||||
buf += `<span class="username clickable" action="open-profile"
|
||||
data-pubkey="${ref}">${fmt_name(profile)}</span>`;
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* fmt_body will parse images, blockquotes, and sanitize the content.
|
||||
*/
|
||||
function fmt_body(content) {
|
||||
return newlines_to_br(linkify(content))
|
||||
}
|
||||
|
||||
/* DEPRECATED: use fmt_name
|
||||
* format_profile_name takes in a profile and tries it's best to
|
||||
* return a string that is best suited for the profile.
|
||||
*/
|
||||
function fmt_profile_name(profile={}, fallback="Anonymous") {
|
||||
const name = profile.display_name || profile.user || profile.name ||
|
||||
fallback
|
||||
return html`${name}`;
|
||||
}
|
||||
|
||||
function fmt_name(profile={data:{}}) {
|
||||
const { data } = profile;
|
||||
const name = data.display_name || data.user || data.name ||
|
||||
fmt_pubkey(profile.pubkey);
|
||||
return html`${name}`;
|
||||
}
|
||||
|
||||
function fmt_pubkey(pk) {
|
||||
if (!pk)
|
||||
return "Unknown";
|
||||
return pk.slice(-8)
|
||||
}
|
||||
|
||||
function fmt_datetime(d) {
|
||||
return d.getFullYear() +
|
||||
"/" + ("0" + (d.getMonth()+1)).slice(-2) +
|
||||
"/" + ("0" + d.getDate()).slice(-2) +
|
||||
" " + ("0" + d.getHours()).slice(-2) +
|
||||
":" + ("0" + d.getMinutes()).slice(-2);
|
||||
}
|
141
js/ui/profile.js
|
@ -1,141 +0,0 @@
|
|||
const PROFILE_FIELDS = [
|
||||
'name',
|
||||
'display_name',
|
||||
'picture',
|
||||
'banner',
|
||||
'website',
|
||||
'nip05',
|
||||
'lud06',
|
||||
'about',
|
||||
];
|
||||
|
||||
function open_profile(pubkey) {
|
||||
view_timeline_apply_mode(DAMUS, VM_USER, { pubkey });
|
||||
}
|
||||
|
||||
function init_profile() {
|
||||
const el = find_node("#profile-info");
|
||||
const el_pfp = find_node("img[name='profile-image']", el);
|
||||
el_pfp.addEventListener("error", onerror_pfp);
|
||||
el_pfp.src = IMG_NO_USER;
|
||||
find_node("button[name='message-user']", el)
|
||||
.addEventListener("click", onclick_message_user);
|
||||
find_node("button[name='edit-profile']", el)
|
||||
.addEventListener("click", onclick_edit_profile);
|
||||
find_node("button[name='copy-pk']", el)
|
||||
.addEventListener("click", onclick_copy_pubkey);
|
||||
find_node("button[name='follow-user']", el)
|
||||
.addEventListener("click", onclick_follow_user);
|
||||
}
|
||||
|
||||
function onclick_message_user(ev) {
|
||||
const pubkey = ev.target.dataset.pubkey;
|
||||
view_timeline_apply_mode(DAMUS, VM_DM_THREAD, { pubkey });
|
||||
}
|
||||
|
||||
/* onclick_copy_pubkey writes the element's dataset.pk field to the users OS'
|
||||
* clipboard. No we don't use fallback APIs, use a recent browser.
|
||||
*/
|
||||
function onclick_copy_pubkey(ev) {
|
||||
const el = ev.target;
|
||||
navigator.clipboard.writeText(el.dataset.pk);
|
||||
// TODO show toast
|
||||
}
|
||||
|
||||
function open_lud06(lud) {
|
||||
navigator.clipboard.writeText(lud);
|
||||
}
|
||||
|
||||
/* onclick_follow_user sends the event to the relay to subscribe the active user
|
||||
* to the target public key of the element's dataset.pk field.
|
||||
*/
|
||||
function onclick_follow_user(ev) {
|
||||
const el = ev.target;
|
||||
const { contacts } = DAMUS;
|
||||
const pubkey = el.dataset.pk;
|
||||
const is_friend = contacts.friends.has(pubkey);
|
||||
if (is_friend) {
|
||||
contacts.friends.delete(pubkey);
|
||||
} else {
|
||||
contacts.friends.add(pubkey);
|
||||
}
|
||||
el.innerText = is_friend ? "Follow" : "Unfollow";
|
||||
contacts_save(contacts);
|
||||
if (window.confirm("Contacts are saved locally. Do you want to sync you contacts with all relays?")) {
|
||||
update_contacts();
|
||||
}
|
||||
}
|
||||
|
||||
function view_update_profile(model, pubkey) {
|
||||
const profile = model_get_profile(model, pubkey);
|
||||
const el = find_node("#profile-info");
|
||||
|
||||
const name = fmt_name(profile);
|
||||
find_node("[name='profile-image']", el).src = get_profile_pic(profile);
|
||||
find_nodes("[name='profile-name']", el).forEach(el => {
|
||||
el.innerText = name;
|
||||
});
|
||||
|
||||
const el_banner = find_node("div[name='profile-banner']", el);
|
||||
el_banner.style.backgroundImage = `url('${profile.data.banner}')`;
|
||||
|
||||
["nip05"].forEach((field)=> {
|
||||
const x = find_node(`[name='profile-${field}']`, el)
|
||||
x.innerText = profile.data[field];
|
||||
x.classList.toggle("hide", !profile.data[field]);
|
||||
});
|
||||
|
||||
const el_lud = find_node(`button[name="profile-lud06"]`, el)
|
||||
el_lud.dataset.lud06 = profile.data.lud06;
|
||||
el_lud.classList.toggle("hide", !profile.data.lud06);
|
||||
el_lud.title = profile.data.lud06;
|
||||
|
||||
const el_website = find_node(`button[name="profile-website"]`, el)
|
||||
el_website.dataset.url = profile.data.website;
|
||||
el_website.classList.toggle("hide", !profile.data.website);
|
||||
el_website.title = profile.data.website;
|
||||
|
||||
const el_desc = find_node("[name='profile-about']", el)
|
||||
el_desc.innerHTML = newlines_to_br(linkify(profile.data.about));
|
||||
el_desc.classList.toggle("hide", !profile.data.about);
|
||||
|
||||
find_node("button[name='copy-pk']", el).dataset.pk = pubkey;
|
||||
find_node("button[name='edit-profile']", el)
|
||||
.classList.toggle("hide", pubkey != model.pubkey);
|
||||
|
||||
const btn_follow = find_node("button[name='follow-user']", el);
|
||||
btn_follow.dataset.pk = pubkey;
|
||||
btn_follow.innerText = contact_is_friend(model.contacts, pubkey) ? "Unfollow" : "Follow";
|
||||
btn_follow.classList.toggle("hide", pubkey == model.pubkey);
|
||||
|
||||
const btn_message = find_node("button[name='message-user']", el);
|
||||
btn_message.dataset.pubkey = pubkey;
|
||||
}
|
||||
|
||||
function onclick_edit_profile() {
|
||||
const p = model_get_profile(DAMUS, DAMUS.pubkey);
|
||||
const el = find_node("#profile-editor");
|
||||
el.showModal();
|
||||
for (const key of PROFILE_FIELDS) {
|
||||
find_node(`[name='${key}']`, el).value = p.data[key] || "";
|
||||
}
|
||||
}
|
||||
|
||||
function click_update_profile() {
|
||||
const el = find_node("#profile-editor");
|
||||
const btn = find_node("button.action", el);
|
||||
const p = Object.assign({}, model_get_profile(DAMUS, DAMUS.pubkey).data, {
|
||||
name: find_node("input[name='name']", el).value,
|
||||
display_name: find_node("input[name='display_name']", el).value,
|
||||
picture: find_node("input[name='picture']", el).value,
|
||||
banner: find_node("input[name='banner']", el).value,
|
||||
website: find_node("input[name='website']", el).value,
|
||||
nip05: find_node("input[name='nip05']", el).value,
|
||||
lud06: find_node("input[name='lud06']", el).value,
|
||||
about: find_node("textarea[name='about']", el).value,
|
||||
});
|
||||
update_profile(p);
|
||||
close_modal(el);
|
||||
// TODO show toast that say's "broadcasted!"
|
||||
}
|
||||
|
270
js/ui/render.js
|
@ -1,270 +0,0 @@
|
|||
// This file contains all methods related to rendering UI elements. Rendering
|
||||
// is done by simple string manipulations & templates. If you need to write
|
||||
// loops simply write it in code and return strings.
|
||||
|
||||
function render_replying_to(model, ev) {
|
||||
if (!(ev.refs && ev.refs.reply))
|
||||
return "";
|
||||
let pubkeys = ev.refs.pubkeys || []
|
||||
if (pubkeys.length === 0 && ev.refs.reply) {
|
||||
const replying_to = model.all_events[ev.refs.reply]
|
||||
// If there is no profile being replied to, it is simply a reply to an
|
||||
// event itself, thus render it differently.
|
||||
if (!replying_to) {
|
||||
return html`<span class="replying-to small-txt">
|
||||
replying in thread
|
||||
<span class="thread-id clickable" action="open-thread"
|
||||
data-thread-id="${ev.refs.reply}">
|
||||
${fmt_pubkey(ev.refs.reply)}</span></span>`;
|
||||
} else {
|
||||
pubkeys = [replying_to.pubkey];
|
||||
}
|
||||
}
|
||||
const names = pubkeys.map((pk) => {
|
||||
return render_name(pk, model_get_profile(model, pk).data);
|
||||
}).join(", ")
|
||||
return `
|
||||
<span class="replying-to small-txt">
|
||||
replying to ${names}
|
||||
</span>
|
||||
`
|
||||
}
|
||||
|
||||
function render_share(model, ev, opts) {
|
||||
const shared_ev = model.all_events[ev.refs && ev.refs.root]
|
||||
// If the shared event hasn't been resolved or leads to a circular event
|
||||
// kind we will skip out on it.
|
||||
if (!shared_ev || shared_ev.kind == KIND_SHARE)
|
||||
return "";
|
||||
opts.shared = {
|
||||
pubkey: ev.pubkey,
|
||||
profile: model_get_profile(model, ev.pubkey),
|
||||
share_time: ev.created_at,
|
||||
share_evid: ev.id,
|
||||
}
|
||||
return render_event(model, shared_ev, opts)
|
||||
}
|
||||
|
||||
function render_shared_by(ev, opts) {
|
||||
if (!opts.shared)
|
||||
return "";
|
||||
const { profile, pubkey } = opts.shared
|
||||
return `<div class="shared-by">Shared by ${render_name(pubkey, profile)}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function render_event(model, ev, opts={}) {
|
||||
switch(ev.kind) {
|
||||
case KIND_SHARE:
|
||||
return render_share(model, ev, opts);
|
||||
case KIND_DM:
|
||||
return render_dm(model, ev, opts);
|
||||
}
|
||||
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
const target_thread_id = ev.refs.root || ev.id;
|
||||
let classes = "event"
|
||||
if (!opts.is_composing)
|
||||
classes += " bottom-border";
|
||||
return html`<div id="ev${ev.id}" class="${classes}" action="open-thread"
|
||||
data-thread-id="${target_thread_id}">
|
||||
$${render_shared_by(ev, opts)}
|
||||
<div class="flex">
|
||||
<div class="userpic">
|
||||
$${render_profile_img(profile)}</div>
|
||||
<div class="event-content">
|
||||
<div class="info">
|
||||
$${render_name(ev.pubkey, profile.data)}
|
||||
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
|
||||
</div>
|
||||
<div class="comment">
|
||||
$${render_event_body(model, ev, opts)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function render_dm(model, ev, opts) {
|
||||
let classes = "event"
|
||||
if (ev.kind == KIND_DM) {
|
||||
classes += " dm";
|
||||
if (ev.pubkey == model.pubkey)
|
||||
classes += " mine";
|
||||
}
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
let show_media = event_shows_media(model, ev, model.embeds);
|
||||
return html`<div id="ev${ev.id}" class="${classes}">
|
||||
<div class="wrap">
|
||||
<div class="body">
|
||||
<p>$${format_content(model, ev, show_media)}</p>
|
||||
</div>
|
||||
<div class="timestamp" data-timestamp="${ev.created_at}">${delta}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function event_shows_media(model, ev, mode) {
|
||||
if (mode == "friends")
|
||||
return model.contacts.friends.has(ev.pubkey);
|
||||
return true;
|
||||
}
|
||||
|
||||
function rerender_dm(model, ev, el) {
|
||||
let show_media = event_shows_media(model, ev, model.embeds);
|
||||
find_node(".body > p", el).innerHTML = format_content(model, ev, show_media);
|
||||
}
|
||||
|
||||
function render_event_nointeract(model, ev, opts={}) {
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
return html`<div class="event border-bottom">
|
||||
<div class="flex">
|
||||
<div class="userpic">
|
||||
$${render_profile_img(profile)}
|
||||
</div>
|
||||
<div class="event-content">
|
||||
<div class="info">
|
||||
$${render_name(ev.pubkey, profile.data)}
|
||||
<span class="timestamp" data-timestamp="${ev.created_at}">${delta}</span>
|
||||
</div>
|
||||
<div class="comment">
|
||||
$${render_event_body(model, ev, opts)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function render_event_body(model, ev, opts) {
|
||||
const { shared } = opts;
|
||||
const can_delete = model.pubkey === ev.pubkey ||
|
||||
(opts.shared && model.pubkey == opts.shared.pubkey);
|
||||
// Only show media for content that is by friends.
|
||||
let show_media = true;
|
||||
if (opts.is_composing) {
|
||||
show_media = false;
|
||||
} else if (model.embeds == "friends") {
|
||||
show_media = model.contacts.friends.has(ev.pubkey);
|
||||
}
|
||||
let str = "<div>";
|
||||
str += render_replying_to(model, ev);
|
||||
str += `</div><p>
|
||||
${format_content(model, ev, show_media)}
|
||||
</p>`;
|
||||
str += render_reactions(model, ev);
|
||||
str += opts.nobar || ev.kind == KIND_DM ? "" :
|
||||
render_action_bar(model, ev, {can_delete, shared});
|
||||
return str;
|
||||
}
|
||||
|
||||
function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) {
|
||||
const reaction = reactions[our_pubkey]
|
||||
if (!reaction) {
|
||||
return html`action="reply" data-emoji="${emoji}" data-to="${reacting_to}"`;
|
||||
} else {
|
||||
return html`action="delete" data-evid="${reaction.id}"`;
|
||||
}
|
||||
}
|
||||
|
||||
function render_reaction_group(model, emoji, reactions, reacting_to) {
|
||||
let count = 0;
|
||||
for (const k in reactions) {
|
||||
count++;
|
||||
}
|
||||
let onclick = render_react_onclick(model.pubkey,
|
||||
reacting_to.id, emoji, reactions);
|
||||
return html`
|
||||
<span $${onclick} class="reaction-group clickable">
|
||||
<span class="reaction-emoji">
|
||||
${emoji}
|
||||
</span>
|
||||
${count}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
function render_action_bar(model, ev, opts={}) {
|
||||
const { pubkey } = model;
|
||||
let { can_delete, shared } = opts;
|
||||
// TODO rewrite all of the toggle heart code. It's mine & I hate it.
|
||||
const thread_root = (ev.refs && ev.refs.root) || ev.id;
|
||||
const reaction = model_get_reacts_to(model, pubkey, ev.id, R_SHAKA);
|
||||
const liked = !!reaction;
|
||||
const reaction_id = reaction ? reaction.id : "";
|
||||
let str = html`<div class="action-bar">`;
|
||||
if (!shared && event_can_reply(ev)) {
|
||||
str += html`
|
||||
<button class="icon" title="Reply" action="reply-to" data-evid="${ev.id}">
|
||||
<img class="icon svg small" src="/icon/event-reply.svg"/>
|
||||
</button>
|
||||
<button class="icon react heart ${ab(liked, 'liked', '')}"
|
||||
action="react-like"
|
||||
data-reaction-id="${reaction_id}"
|
||||
data-reacting-to="${ev.id}"
|
||||
title="$${ab(liked, 'Unlike', 'Like')}">
|
||||
<img class="icon svg small ${ab(liked, 'dark-noinvert', '')}"
|
||||
src="$${ab(liked, IMG_EVENT_LIKED, IMG_EVENT_LIKE)}"/>
|
||||
</button>`;
|
||||
}
|
||||
if (!shared) {
|
||||
str += html`<button class="icon" title="Share" data-evid="${ev.id}"
|
||||
action="share">
|
||||
<img class="icon svg small" src="/icon/event-share.svg"/>
|
||||
</button>`;
|
||||
}
|
||||
str += `
|
||||
<button class="icon" title="More Options" action="open-event-options">
|
||||
<img class="icon svg small" src="/icon/event-options.svg"/>
|
||||
</button>`;
|
||||
return str + "</div>";
|
||||
}
|
||||
|
||||
function render_reactions_inner(model, ev) {
|
||||
const groups = get_reactions(model, ev.id)
|
||||
let str = ""
|
||||
for (const emoji of Object.keys(groups)) {
|
||||
str += render_reaction_group(model, emoji, groups[emoji], ev)
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function render_reactions(model, ev) {
|
||||
return html`<div class="reactions">$${render_reactions_inner(model, ev)}</div>`
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
|
||||
function render_pubkey(pk) {
|
||||
return fmt_pubkey(pk);
|
||||
}
|
||||
|
||||
function render_username(pk, profile) {
|
||||
return (profile && profile.name) || render_pubkey(pk)
|
||||
}
|
||||
|
||||
function render_mentioned_name(pk, profile) {
|
||||
return render_name(pk, profile, "");
|
||||
}
|
||||
|
||||
function render_name(pk, profile, prefix="") {
|
||||
// Beware of whitespace.
|
||||
return html`<span>${prefix}<span class="username clickable"
|
||||
action="open-profile" data-pubkey="${pk}">
|
||||
${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
|
||||
}
|
||||
|
||||
function render_profile_img(profile, noclick=false) {
|
||||
const name = fmt_name(profile);
|
||||
let str = html`class="pfp clickable" action="open-profile"`;
|
||||
if (noclick)
|
||||
str = "class='pfp'";
|
||||
return html`<img
|
||||
$${str}
|
||||
data-pubkey="${profile.pubkey}"
|
||||
title="${name}"
|
||||
src="${get_profile_pic(profile)}"/>`
|
||||
//onerror="this.onerror=null;this.src='${IMG_NO_USER}';"
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// https://github.com/AntonioVdlC/html-template-tag
|
||||
|
||||
const chars = {
|
||||
"&": "&",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`",
|
||||
};
|
||||
|
||||
// Dynamically create a RegExp from the `chars` object
|
||||
const re = new RegExp(Object.keys(chars).join("|"), "g");
|
||||
|
||||
// Return the escaped string
|
||||
function escape(str) {
|
||||
return String(str).replace(re, (match) => chars[match]);
|
||||
}
|
||||
|
||||
function html(
|
||||
literals,
|
||||
...substs
|
||||
) {
|
||||
return literals.raw.reduce((acc, lit, i) => {
|
||||
let subst = substs[i - 1];
|
||||
if (Array.isArray(subst)) {
|
||||
subst = subst.join("");
|
||||
} else if (literals.raw[i - 1] && literals.raw[i - 1].endsWith("$")) {
|
||||
// If the interpolation is preceded by a dollar sign,
|
||||
// substitution is considered safe and will not be escaped
|
||||
acc = acc.slice(0, -1);
|
||||
} else {
|
||||
subst = escape(subst);
|
||||
}
|
||||
|
||||
return acc + subst + lit;
|
||||
});
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
function init_settings(model) {
|
||||
const el = find_node("#settings");
|
||||
find_node("#add-relay", el).addEventListener("click", on_click_add_relay);
|
||||
const rlist = find_node("#relay-list tbody", el);
|
||||
model.relays.forEach((str) => {
|
||||
rlist.appendChild(new_relay_item(str));
|
||||
});
|
||||
}
|
||||
|
||||
function new_relay_item(str) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${str}</td>
|
||||
<td>
|
||||
<button class="remove-relay btn-text"
|
||||
data-address="${str}"
|
||||
role="remove-relay">
|
||||
<img class="icon svg small" src="/icon/event-delete.svg"/>
|
||||
</button>
|
||||
</td>`;
|
||||
find_node("button", tr).addEventListener("click", on_click_remove_relay);
|
||||
return tr;
|
||||
}
|
||||
|
||||
function on_click_add_relay(ev) {
|
||||
const model = DAMUS;
|
||||
const address = prompt("Please provide a websocket address:", "wss://");
|
||||
log_debug("got address", address);
|
||||
// TODO add relay validation
|
||||
if (!model.pool.add(address))
|
||||
return;
|
||||
model.relays.add(address);
|
||||
find_node("#relay-list tbody").appendChild(new_relay_item(address));
|
||||
model_save_settings(model);
|
||||
}
|
||||
|
||||
function on_click_remove_relay(ev) {
|
||||
const model = DAMUS;
|
||||
const address = ev.target.dataset.address;
|
||||
if (!model.pool.remove(address))
|
||||
return;
|
||||
model.relays.delete(address);
|
||||
let parent = ev.target;
|
||||
while (parent) {
|
||||
if (parent.matches("tr")) {
|
||||
parent.parentElement.removeChild(parent);
|
||||
break;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
model_save_settings(model);
|
||||
}
|
777
js/ui/state.js
|
@ -1,777 +0,0 @@
|
|||
const VM_FRIENDS = "friends"; // mine + only events that are from my contacts
|
||||
const VM_NOTIFICATIONS = "notifications"; // reactions & replys
|
||||
const VM_DM = "dm"; // all events of KIND_DM aimmed at user
|
||||
const VM_DM_THREAD = "dmthread"; // all events from a user of KIND_DM
|
||||
const VM_THREAD = "thread"; // all events in response to target event
|
||||
const VM_USER = "user"; // all events by pubkey
|
||||
const VM_SETTINGS = "settings";
|
||||
|
||||
const VIEW_NAMES= {};
|
||||
VIEW_NAMES[VM_FRIENDS] = "Home";
|
||||
VIEW_NAMES[VM_NOTIFICATIONS] = "Notifications";
|
||||
VIEW_NAMES[VM_DM] = "Messages";
|
||||
VIEW_NAMES[VM_DM_THREAD] = "DM";
|
||||
VIEW_NAMES[VM_USER] = "Profile";
|
||||
VIEW_NAMES[VM_THREAD] = "Thread";
|
||||
VIEW_NAMES[VM_SETTINGS] = "Settings";
|
||||
|
||||
function view_get_timeline_el() {
|
||||
return find_node("#timeline");
|
||||
}
|
||||
|
||||
// TODO clean up popstate listener (move to init method or such)
|
||||
window.addEventListener("popstate", function(event) {
|
||||
if (event.state && event.state.mode) {
|
||||
// Update the timeline mode.
|
||||
// Pass pushState=false to avoid adding another state to the history
|
||||
view_timeline_apply_mode(DAMUS, event.state.mode, event.state.opts, false);
|
||||
}
|
||||
})
|
||||
|
||||
function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
||||
let xs;
|
||||
const { pubkey, thread_id } = opts;
|
||||
const el = view_get_timeline_el();
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (opts.hide_replys == undefined) {
|
||||
opts.hide_replys = el.dataset.hideReplys == "true";
|
||||
}
|
||||
|
||||
// Don't do anything if we are already here
|
||||
if (el.dataset.mode == mode) {
|
||||
switch (mode) {
|
||||
case VM_FRIENDS:
|
||||
if ((el.dataset.hideReplys == "true") == opts.hide_replys)
|
||||
return;
|
||||
push_state = false;
|
||||
break;
|
||||
case VM_DM_THREAD:
|
||||
case VM_USER:
|
||||
if (el.dataset.pubkey == opts.pubkey)
|
||||
return;
|
||||
break;
|
||||
case VM_THREAD:
|
||||
if (el.dataset.threadId == thread_id)
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch history for certain views
|
||||
if (mode == VM_THREAD) {
|
||||
view_show_spinner(true);
|
||||
fetch_thread_history(thread_id, model.pool);
|
||||
}
|
||||
if (mode == VM_USER && pubkey && pubkey != model.pubkey) {
|
||||
view_show_spinner(true);
|
||||
fetch_profile(pubkey, model.pool);
|
||||
}
|
||||
if (mode == VM_NOTIFICATIONS) {
|
||||
reset_notifications(model);
|
||||
}
|
||||
|
||||
const names = VIEW_NAMES;
|
||||
let name = names[mode];
|
||||
let profile;
|
||||
|
||||
// Push a new state to the browser history stack
|
||||
if (push_state) {
|
||||
let pieces = [name.toLowerCase()];
|
||||
switch (mode) {
|
||||
case VM_FRIENDS:
|
||||
pieces = [];
|
||||
break;
|
||||
case VM_THREAD:
|
||||
pieces.push(thread_id);
|
||||
break;
|
||||
case VM_USER:
|
||||
case VM_DM_THREAD:
|
||||
pieces.push(pubkey);
|
||||
break;
|
||||
}
|
||||
window.history.pushState({mode, opts}, "", "/"+pieces.join("/"));
|
||||
}
|
||||
|
||||
el.dataset.mode = mode;
|
||||
delete el.dataset.threadId;
|
||||
delete el.dataset.pubkey;
|
||||
switch(mode) {
|
||||
case VM_FRIENDS:
|
||||
el.dataset.hideReplys = opts.hide_replys;
|
||||
break;
|
||||
case VM_THREAD:
|
||||
el.dataset.threadId = thread_id;
|
||||
break;
|
||||
case VM_USER:
|
||||
case VM_DM_THREAD:
|
||||
profile = model_get_profile(model, pubkey);
|
||||
name = fmt_name(profile);
|
||||
el.dataset.pubkey = pubkey;
|
||||
break;
|
||||
}
|
||||
|
||||
// Do some visual updates
|
||||
find_node("#show-more").classList.add("hide");
|
||||
find_node("#view header > label").innerText = name;
|
||||
view_update_navs(mode);
|
||||
find_node("#view [role='profile-info']").classList.toggle("hide", mode != VM_USER);
|
||||
const timeline_el = find_node("#timeline");
|
||||
timeline_el.classList.toggle("reverse", mode == VM_DM_THREAD);
|
||||
timeline_el.classList.toggle("hide", mode == VM_SETTINGS || mode == VM_DM);
|
||||
find_node("#settings").classList.toggle("hide", mode != VM_SETTINGS);
|
||||
find_node("#dms").classList.toggle("hide", mode != VM_DM);
|
||||
find_node("#dm-post").classList.toggle("hide", mode != VM_DM_THREAD);
|
||||
find_node("#new-note-mobile").classList.toggle("hide", mode == VM_DM_THREAD);
|
||||
find_node("#header-tools button[action='mark-all-read']")
|
||||
.classList.toggle("hide", mode != VM_DM);
|
||||
|
||||
// Show/hide different profile image in header
|
||||
const show_mypfp = mode != VM_DM_THREAD && mode != VM_USER;
|
||||
const el_their_pfp = find_node("#view header img.pfp[role='their-pfp']");
|
||||
el_their_pfp.classList.toggle("hide", show_mypfp);
|
||||
find_node("#view header img.pfp[role='my-pfp']")
|
||||
.classList.toggle("hide", !show_mypfp);
|
||||
|
||||
view_timeline_refresh(model, mode, opts);
|
||||
|
||||
switch (mode) {
|
||||
case VM_DM_THREAD:
|
||||
decrypt_dms(model);
|
||||
model_dm_seen(model, pubkey);
|
||||
el_their_pfp.src = get_profile_pic(profile);
|
||||
el_their_pfp.dataset.pubkey = pubkey;
|
||||
break;
|
||||
case VM_DM:
|
||||
model.dms_need_redraw = true;
|
||||
view_show_spinner(true);
|
||||
view_set_show_count(0, true, true);
|
||||
//decrypt_dms(model);
|
||||
//view_dm_update(model);
|
||||
break;
|
||||
case VM_SETTINGS:
|
||||
view_show_spinner(false);
|
||||
view_set_show_count(0, true, true);
|
||||
break;
|
||||
case VM_USER:
|
||||
el_their_pfp.src = get_profile_pic(profile);
|
||||
el_their_pfp.dataset.pubkey = pubkey;
|
||||
view_update_profile(model, pubkey);
|
||||
break;
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/* view_timeline_refresh is a hack for redrawing the events in order
|
||||
*/
|
||||
function view_timeline_refresh(model, mode, opts={}) {
|
||||
const el = view_get_timeline_el();
|
||||
if (!mode) {
|
||||
mode = el.dataset.mode;
|
||||
opts.thread_id = el.dataset.threadId;
|
||||
opts.pubkey = el.dataset.pubkey;
|
||||
opts.hide_replys = el.dataset.hideReplys == "true";
|
||||
}
|
||||
// Remove all
|
||||
// This is faster than removing one by one
|
||||
el.innerHTML = "";
|
||||
// Build DOM fragment and render it
|
||||
let count = 0;
|
||||
const limit = 200;
|
||||
let evs = mode == VM_DM_THREAD ?
|
||||
model_get_dm(model, opts.pubkey).events.concat().reverse() :
|
||||
model_events_arr(model);
|
||||
if (mode == VM_THREAD)
|
||||
evs = evs.reverse();
|
||||
const fragment = new DocumentFragment();
|
||||
let show_more = true;
|
||||
let i = evs.length - 1;
|
||||
for (; i >= 0 && count < limit; i--) {
|
||||
const ev = evs[i];
|
||||
if (!view_mode_contains_event(model, ev, mode, opts))
|
||||
continue;
|
||||
let ev_el = model.elements[ev.id];
|
||||
if (!ev_el)
|
||||
continue;
|
||||
fragment.appendChild(ev_el);
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
el.append(fragment);
|
||||
view_set_show_count(0);
|
||||
view_timeline_update_timestamps();
|
||||
view_show_spinner(false);
|
||||
}
|
||||
// Reduce i to 0 if there are no more events to show, otherwise exit at
|
||||
// first instance. Determine show_more base on these facts
|
||||
for (; i > 0; i--) {
|
||||
if (view_mode_contains_event(model, evs[i], mode, opts))
|
||||
break;
|
||||
}
|
||||
if (i < 0 || count < limit)
|
||||
show_more = false;
|
||||
// If we reached the limit there is "probably" more to show so show
|
||||
// the more button
|
||||
const is_more_mode = mode == VM_FRIENDS || mode == VM_NOTIFICATIONS;
|
||||
if (is_more_mode && show_more) {
|
||||
find_node("#show-more").classList.remove("hide");
|
||||
}
|
||||
}
|
||||
|
||||
function view_update_navs(mode) {
|
||||
find_nodes("nav.nav button[data-view]").forEach((el)=> {
|
||||
el.classList.toggle("active", el.dataset.view == mode)
|
||||
});
|
||||
}
|
||||
|
||||
function view_show_spinner(show=true) {
|
||||
find_node("#view .loading-events").classList.toggle("hide", !show);
|
||||
}
|
||||
|
||||
function view_get_el_opts(el) {
|
||||
const mode = el.dataset.mode;
|
||||
return {
|
||||
thread_id: el.dataset.threadId,
|
||||
pubkey: el.dataset.pubkey,
|
||||
hide_replys: mode == VM_FRIENDS && el.dataset.hideReplys == "true",
|
||||
};
|
||||
}
|
||||
|
||||
/* view_timeline_update iterates through invalidated event ids and updates the
|
||||
* state of the timeline and other factors such as notifications, etc.
|
||||
*/
|
||||
function view_timeline_update(model) {
|
||||
const el = view_get_timeline_el();
|
||||
const mode = el.dataset.mode;
|
||||
const opts = view_get_el_opts(el);
|
||||
let count = 0;
|
||||
let ncount = 0;
|
||||
let decrypted = false;
|
||||
const latest_ev = el.firstChild ?
|
||||
model.all_events[el.firstChild.id.slice(2)] : undefined;
|
||||
const left_overs = [];
|
||||
while (model.invalidated.length > 0 && count < 500) {
|
||||
var evid = model.invalidated.pop();
|
||||
|
||||
// Remove deleted events first
|
||||
if (model_is_event_deleted(model, evid)) {
|
||||
let x = model.elements[evid];
|
||||
if (x && x.parentElement) {
|
||||
x.parentElement.removeChild(x);
|
||||
delete model.elements[evid];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip non-renderables
|
||||
var ev = model.all_events[evid];
|
||||
if (!event_is_renderable(ev)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-render content of a decrypted dm
|
||||
if (ev.kind == KIND_DM && model.elements[evid]) {
|
||||
rerender_dm(model, ev, model.elements[evid]);
|
||||
decrypted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Put it back on the stack to re-render if it's not ready.
|
||||
if (!view_render_event(model, ev)) {
|
||||
left_overs.push(evid);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Increase notification count if needed
|
||||
if (event_refs_pubkey(ev, model.pubkey) &&
|
||||
ev.created_at > model.notifications.last_viewed) {
|
||||
ncount++;
|
||||
}
|
||||
|
||||
// If the new element is newer than the latest & is viewable then
|
||||
// we want to increase the count of how many to add to view
|
||||
if (event_cmp_created(ev, latest_ev) >= 0 &&
|
||||
view_mode_contains_event(model, ev, mode, opts)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
model.invalidated = model.invalidated.concat(left_overs);
|
||||
|
||||
// If there are new things to show on our current view lets do it
|
||||
if (count > 0) {
|
||||
if (!latest_ev || mode == VM_DM_THREAD) {
|
||||
view_timeline_show_new(model);
|
||||
}
|
||||
if (mode == VM_DM_THREAD) {
|
||||
model_mark_dms_seen(model, opts.pubkey);
|
||||
view_dm_update(model);
|
||||
}
|
||||
view_set_show_count(count, true, false);
|
||||
}
|
||||
// Update notification markers and count
|
||||
if (ncount > 0) {
|
||||
//log_debug(`new notis ${ncount}`);
|
||||
model.notifications.count += ncount;
|
||||
}
|
||||
// Update the dms list view
|
||||
if (decrypted) {
|
||||
view_dm_update(model);
|
||||
}
|
||||
}
|
||||
|
||||
function view_set_show_count(count, add=false, hide=false) {
|
||||
const show_el = find_node("#show-new")
|
||||
const num_el = find_node("#show-new span", show_el);
|
||||
if (add) {
|
||||
count += parseInt(num_el.innerText || 0)
|
||||
}
|
||||
num_el.innerText = count;
|
||||
show_el.classList.toggle("hide", hide || count <= 0);
|
||||
}
|
||||
|
||||
function view_timeline_show_new(model) {
|
||||
const el = view_get_timeline_el();
|
||||
const mode = el.dataset.mode;
|
||||
const opts = view_get_el_opts(el);
|
||||
let latest_evid = el.firstChild ? el.firstChild.id.slice(2) : undefined;
|
||||
if (mode == VM_THREAD) {
|
||||
latest_evid = el.lastElementChild ? el.lastElementChild.id.slice(2) : undefined;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
const evs = model_events_arr(model)
|
||||
const fragment = new DocumentFragment();
|
||||
for (let i = evs.length - 1; i >= 0 && count < 500; i--) {
|
||||
const ev = evs[i];
|
||||
if (latest_evid && ev.id == latest_evid) {
|
||||
break;
|
||||
}
|
||||
if (!view_mode_contains_event(model, ev, mode, opts))
|
||||
continue;
|
||||
let ev_el = model.elements[ev.id];
|
||||
if (!ev_el)
|
||||
continue;
|
||||
fragment.appendChild(ev_el);
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
if (mode == VM_THREAD) {
|
||||
el.appendChild(fragment);
|
||||
} else {
|
||||
el.prepend(fragment);
|
||||
}
|
||||
view_show_spinner(false);
|
||||
if (mode == VM_NOTIFICATIONS) {
|
||||
reset_notifications(model);
|
||||
}
|
||||
}
|
||||
view_set_show_count(-count, true);
|
||||
view_timeline_update_timestamps();
|
||||
if (mode == VM_DM_THREAD) decrypt_dms(model);
|
||||
}
|
||||
|
||||
function view_timeline_show_more(model) {
|
||||
const el = view_get_timeline_el();
|
||||
const mode = el.dataset.mode;
|
||||
const opts = view_get_el_opts(el);
|
||||
const oldest_evid = el.lastElementChild ? el.lastElementChild.id.slice(2) : undefined;
|
||||
const oldest = model.all_events[oldest_evid];
|
||||
const evs = model_events_arr(model);
|
||||
const fragment = new DocumentFragment();
|
||||
let i = arr_bsearch(evs, oldest, (a, b) => {
|
||||
if (a.id == b.id || a.created_at == b.created_at)
|
||||
return 0;
|
||||
if (a.created_at > b.created_at)
|
||||
return 1;
|
||||
return -1;
|
||||
});
|
||||
const limit = 200;
|
||||
let count = 0;
|
||||
for (; i >= 0 && count < limit; i--){
|
||||
const ev = evs[i];
|
||||
if (!view_mode_contains_event(model, ev, mode, opts))
|
||||
continue;
|
||||
let ev_el = model.elements[ev.id];
|
||||
if (!ev_el || ev_el.parentElement)
|
||||
continue;
|
||||
fragment.appendChild(ev_el);
|
||||
count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
el.append(fragment);
|
||||
}
|
||||
if (count < limit) {
|
||||
// No more to show, hide the button
|
||||
find_node("#show-more").classList.add("hide");
|
||||
}
|
||||
view_timeline_update_timestamps();
|
||||
}
|
||||
|
||||
function view_render_event(model, ev, force=false) {
|
||||
if (model.elements[ev.id] && !force)
|
||||
return model.elements[ev.id];
|
||||
const html = render_event(model, ev, {});
|
||||
if (html == "") {
|
||||
//log_debug(`failed to render ${ev.id}`);
|
||||
return;
|
||||
}
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
const el = div.firstChild;
|
||||
model.elements[ev.id] = el;
|
||||
const pfp = find_node("img.pfp", el)
|
||||
if (pfp)
|
||||
pfp.addEventListener("error", onerror_pfp);
|
||||
return el;
|
||||
}
|
||||
|
||||
function view_timeline_update_profiles(model, pubkey) {
|
||||
const el = view_get_timeline_el();
|
||||
const p = model_get_profile(model, pubkey);
|
||||
const name = fmt_name(p);
|
||||
const pic = get_profile_pic(p);
|
||||
for (const evid in model.elements) {
|
||||
// XXX if possible update profile pics in a smarter way
|
||||
// this may be perhaps a micro optimization tho
|
||||
update_el_profile(model.elements[evid], pubkey, name, pic);
|
||||
}
|
||||
// Update the profile view if it's active
|
||||
if (el.dataset.pubkey == pubkey) {
|
||||
const mode = el.dataset.mode;
|
||||
switch (mode) {
|
||||
case VM_USER:
|
||||
view_update_profile(model, pubkey);
|
||||
case VM_DM_THREAD:
|
||||
find_node("#view header > label").innerText = name;
|
||||
}
|
||||
}
|
||||
// Update dm's section since they are not in our view, dm's themselves will
|
||||
// be caught by the process above.
|
||||
update_el_profile(find_node("#dms"), pubkey, name, pic);
|
||||
update_el_profile(find_node("#view header"), pubkey, name, pic);
|
||||
}
|
||||
|
||||
function update_el_profile(el, pubkey, name, pic) {
|
||||
if (!el)
|
||||
return;
|
||||
find_nodes(`.username[data-pubkey='${pubkey}']`, el).forEach((el)=> {
|
||||
el.innerText = name;
|
||||
});
|
||||
find_nodes(`img[data-pubkey='${pubkey}']`, el).forEach((el)=> {
|
||||
el.src = pic;
|
||||
el.title = name;
|
||||
});
|
||||
}
|
||||
|
||||
function view_timeline_update_timestamps() {
|
||||
// TODO only update elements that are fresh and are in DOM
|
||||
const el = view_get_timeline_el();
|
||||
let xs = el.querySelectorAll(".timestamp");
|
||||
let now = new Date().getTime();
|
||||
for (const x of xs) {
|
||||
let t = parseInt(x.dataset.timestamp)
|
||||
x.innerText = fmt_since_str(now, t*1000);
|
||||
}
|
||||
}
|
||||
|
||||
function view_timeline_update_reaction(model, ev) {
|
||||
let el;
|
||||
const o = event_parse_reaction(ev);
|
||||
if (!o)
|
||||
return;
|
||||
const ev_id = o.e;
|
||||
const root = model.elements[ev_id];
|
||||
if (!root)
|
||||
return;
|
||||
|
||||
// Update reaction groups
|
||||
el = find_node(`.reactions`, root);
|
||||
el.innerHTML = render_reactions_inner(model, model.all_events[ev_id]);
|
||||
|
||||
// Update like button
|
||||
if (ev.pubkey == model.pubkey) {
|
||||
const reaction = model_get_reacts_to(model, model.pubkey, ev_id, R_SHAKA);
|
||||
const liked = !!reaction;
|
||||
const img = find_node("button.icon.heart > img", root);
|
||||
const btn = find_node("button.icon.heart", root)
|
||||
btn.classList.toggle("liked", liked);
|
||||
btn.title = liked ? "Unlike" : "Like";
|
||||
btn.disabled = false;
|
||||
btn.dataset.liked = liked ? "yes" : "no";
|
||||
btn.dataset.reactionId = liked ? reaction.id : "";
|
||||
img.classList.toggle("dark-noinvert", liked);
|
||||
img.src = liked ? IMG_EVENT_LIKED : IMG_EVENT_LIKE;
|
||||
}
|
||||
}
|
||||
|
||||
function view_mode_contains_event(model, ev, mode, opts={}) {
|
||||
if (mode != VM_DM_THREAD && ev.kind == KIND_DM) {
|
||||
return false;
|
||||
}
|
||||
switch(mode) {
|
||||
case VM_USER:
|
||||
return opts.pubkey && ev.pubkey == opts.pubkey;
|
||||
case VM_FRIENDS:
|
||||
if (opts.hide_replys && event_is_reply(ev))
|
||||
return false;
|
||||
return ev.pubkey == model.pubkey || contact_is_friend(model.contacts, ev.pubkey);
|
||||
case VM_THREAD:
|
||||
if (ev.kind == KIND_SHARE) return false;
|
||||
return ev.id == opts.thread_id ||
|
||||
event_refs_event(ev, {id:opts.thread_id});
|
||||
case VM_NOTIFICATIONS:
|
||||
return event_tags_pubkey(ev, model.pubkey);
|
||||
case VM_DM_THREAD:
|
||||
if (ev.kind != KIND_DM) return false;
|
||||
return (ev.pubkey == opts.pubkey &&
|
||||
event_tags_pubkey(ev, model.pubkey)) ||
|
||||
(ev.pubkey == model.pubkey &&
|
||||
event_tags_pubkey(ev, opts.pubkey));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function event_is_renderable(ev={}) {
|
||||
return ev.kind == KIND_NOTE || ev.kind == KIND_SHARE || ev.kind == KIND_DM;
|
||||
}
|
||||
|
||||
function get_default_max_depth(damus, view) {
|
||||
return view.max_depth || damus.max_depth
|
||||
}
|
||||
|
||||
function get_thread_max_depth(damus, view, root_id) {
|
||||
if (!view.depths[root_id])
|
||||
return get_default_max_depth(damus, view);
|
||||
return view.depths[root_id];
|
||||
}
|
||||
|
||||
function get_thread_root_id(damus, id) {
|
||||
const ev = damus.all_events[id];
|
||||
if (!ev) {
|
||||
log_debug("expand_thread: no event found?", id)
|
||||
return null;
|
||||
}
|
||||
return ev.refs && ev.refs.root;
|
||||
}
|
||||
|
||||
function switch_view(mode, opts) {
|
||||
view_timeline_apply_mode(DAMUS, mode, opts);
|
||||
}
|
||||
|
||||
function toggle_hide_replys(el) {
|
||||
const hide = el.innerText == "Hide Replys";
|
||||
switch_view(VM_FRIENDS, {hide_replys: hide});
|
||||
el.innerText = hide ? "Show Replys" : "Hide Replys";
|
||||
}
|
||||
|
||||
function reset_notifications(model) {
|
||||
model.notifications.count = 0;
|
||||
model.notifications.last_viewed = new_creation_time();
|
||||
update_notifications(model);
|
||||
}
|
||||
|
||||
function html2el(html) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
function init_timeline(model) {
|
||||
const el = view_get_timeline_el();
|
||||
el.addEventListener("click", onclick_timeline);
|
||||
}
|
||||
function onclick_timeline(ev) {
|
||||
if (ev.target.matches(".username[data-pubkey]")) {
|
||||
open_profile(ev.target.dataset.pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
function init_my_pfp(model) {
|
||||
find_nodes(`img[role='my-pfp']`).forEach((el)=> {
|
||||
el.dataset.pubkey = model.pubkey;
|
||||
el.addEventListener("error", onerror_pfp);
|
||||
el.addEventListener("click", onclick_pfp);
|
||||
el.classList.add("clickable");
|
||||
});
|
||||
find_nodes(`img[role='their-pfp']`).forEach((el)=> {
|
||||
el.addEventListener("error", onerror_pfp);
|
||||
el.addEventListener("click", onclick_pfp);
|
||||
el.classList.add("clickable");
|
||||
});
|
||||
}
|
||||
|
||||
function init_postbox(model) {
|
||||
find_node("#reply-content").addEventListener("input", oninput_post);
|
||||
find_node("#dm-post textarea").addEventListener("input", oninput_post);
|
||||
find_node("button[name='reply']")
|
||||
.addEventListener("click", onclick_reply);
|
||||
find_node("button[name='reply-all']")
|
||||
.addEventListener("click", onclick_reply);
|
||||
find_node("button[name='send']")
|
||||
.addEventListener("click", onclick_send);
|
||||
find_node("button[name='send-dm']")
|
||||
.addEventListener("click", onclick_send_dm);
|
||||
}
|
||||
async function onclick_reply(ev) {
|
||||
do_send_reply(ev.target.dataset.all == "1");
|
||||
}
|
||||
async function onclick_send(ev) {
|
||||
const el = find_node("#reply-modal");
|
||||
const pubkey = await get_pubkey();
|
||||
const el_input = el.querySelector("#reply-content");
|
||||
let post = {
|
||||
pubkey,
|
||||
kind: KIND_NOTE,
|
||||
created_at: new_creation_time(),
|
||||
content: el_input.value,
|
||||
tags: [],
|
||||
};
|
||||
post.id = await nostrjs.calculate_id(post);
|
||||
post = await sign_event(post);
|
||||
broadcast_event(post);
|
||||
|
||||
// Reset UI
|
||||
el_input.value = "";
|
||||
trigger_postbox_assess(el_input);
|
||||
close_modal(el);
|
||||
}
|
||||
async function onclick_send_dm(ev) {
|
||||
const pubkey = await get_pubkey();
|
||||
const el = find_node("#dm-post");
|
||||
const el_input = el.querySelector("textarea");
|
||||
const target = view_get_timeline_el().dataset.pubkey;
|
||||
let post = {
|
||||
pubkey,
|
||||
kind: KIND_DM,
|
||||
created_at: new_creation_time(),
|
||||
content: await window.nostr.nip04.encrypt(target, el_input.value),
|
||||
tags: [["p", target]],
|
||||
};
|
||||
post.id = await nostrjs.calculate_id(post);
|
||||
post = await sign_event(post);
|
||||
broadcast_event(post);
|
||||
|
||||
el_input.value = "";
|
||||
trigger_postbox_assess(el_input);
|
||||
}
|
||||
/* oninput_post checks the content of the textarea and updates the size
|
||||
* of it's element. Additionally I will toggle the enabled state of the sending
|
||||
* button.
|
||||
*/
|
||||
function oninput_post(ev) {
|
||||
trigger_postbox_assess(ev.target);
|
||||
}
|
||||
function trigger_postbox_assess(el) {
|
||||
el.style.height = `0px`;
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
let btn = el.parentElement.querySelector("button[role=send]");
|
||||
if (btn) btn.disabled = el.value === "";
|
||||
}
|
||||
/* toggle_cw changes the active stage of the Content Warning for a post. It is
|
||||
* relative to the element that is pressed.
|
||||
*/
|
||||
function onclick_toggle_cw(ev) {
|
||||
const el = ev.target;
|
||||
el.classList.toggle("active");
|
||||
const isOn = el.classList.contains("active");
|
||||
const input = el.parentElement.querySelector("input.cw");
|
||||
input.classList.toggle("hide", !isOn);
|
||||
}
|
||||
|
||||
function onclick_any(ev) {
|
||||
let el = ev.target;
|
||||
// Check if we have a selection and don't bother with anything
|
||||
let selection = document.getSelection();
|
||||
if (selection && selection.isCollapsed == false &&
|
||||
view_get_timeline_el().contains(selection.anchorNode)) {
|
||||
return;
|
||||
}
|
||||
let action = el.getAttribute("action");
|
||||
if (action == null && el.tagName != "A") {
|
||||
const parent = find_parent(el, "[action]");
|
||||
if (parent) {
|
||||
const parent_action = parent.getAttribute("action")
|
||||
// This is a quick hijack for propogating clicks; further extending
|
||||
// this should be obvious.
|
||||
if (parent_action == "open-thread") {
|
||||
el = parent;
|
||||
action = parent_action;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (action) {
|
||||
case "sign-in":
|
||||
signin();
|
||||
break;
|
||||
case "open-view":
|
||||
switch_view(el.dataset.view);
|
||||
break;
|
||||
case "close-media":
|
||||
close_media_preview();
|
||||
break;
|
||||
case "close-modal":
|
||||
close_modal(el);
|
||||
break;
|
||||
case "open-profile":
|
||||
open_profile(el.dataset.pubkey);
|
||||
break;
|
||||
case "open-profile-editor":
|
||||
click_update_profile();
|
||||
break;
|
||||
case "show-timeline-new":
|
||||
show_new();
|
||||
break;
|
||||
case "show-timeline-more":
|
||||
view_timeline_show_more(DAMUS);
|
||||
break;
|
||||
case "open-thread":
|
||||
open_thread(el.dataset.threadId);
|
||||
break;
|
||||
case "reply":
|
||||
send_reply(el.dataset.emoji, el.dataset.to);
|
||||
break;
|
||||
case "delete":
|
||||
delete_post(el.dataset.evid);
|
||||
break;
|
||||
case "reply-to":
|
||||
reply(el.dataset.evid);
|
||||
break;
|
||||
case "react-like":
|
||||
click_toggle_like(el);
|
||||
break;
|
||||
case "share":
|
||||
click_share(el);
|
||||
break;
|
||||
case "open-thread":
|
||||
open_thread(el.dataset.threadId);
|
||||
break;
|
||||
case "open-media":
|
||||
open_media_preview(el.src, el.dataset.type);
|
||||
break;
|
||||
case "open-link":
|
||||
window.open(el.dataset.url, "_blank");
|
||||
break;
|
||||
case "open-lud06":
|
||||
open_lud06(el.dataset.lud06);
|
||||
break;
|
||||
case "show-event-json":
|
||||
on_click_show_event_details(el.dataset.evid);
|
||||
break;
|
||||
case "confirm-delete":
|
||||
delete_post_confirm(el.dataset.evid);
|
||||
break;
|
||||
case "mark-all-read":
|
||||
model_mark_dms_seen(DAMUS);
|
||||
break;
|
||||
case "toggle-hide-replys":
|
||||
toggle_hide_replys(el);
|
||||
break;
|
||||
case "new-note":
|
||||
new_note();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
196
js/ui/util.js
|
@ -1,196 +0,0 @@
|
|||
/* This file contains utility functions related to UI manipulation. Some code
|
||||
* may be specific to areas of the UI and others are more utility based. As
|
||||
* this file grows specific UI area code should be migrated to its own file.
|
||||
*/
|
||||
|
||||
/* init_message_textareas finds all message textareas and updates their initial
|
||||
* height based on their content (0). This is so there is no jaring affect when
|
||||
* the page loads.
|
||||
*/
|
||||
function init_message_textareas() {
|
||||
const els = document.querySelectorAll(".post-input");
|
||||
for (const el of els) {
|
||||
trigger_postbox_assess(el);
|
||||
}
|
||||
}
|
||||
|
||||
// update_notification_markers will find all markers and hide or show them
|
||||
// based on the passed in state of 'active'. This applies to the navigation
|
||||
// icons.
|
||||
function update_notification_markers(active, type) {
|
||||
let els = document.querySelectorAll(`.new-notifications[role='${type}']`)
|
||||
for (const el of els) {
|
||||
el.classList.toggle("hide", !active)
|
||||
}
|
||||
}
|
||||
|
||||
/* newlines_to_br takes a string and converts all newlines to HTML 'br' tags.
|
||||
*/
|
||||
function newlines_to_br(str="") {
|
||||
return str.split("\n").reduce((acc, part, index) => {
|
||||
return acc + part + "<br/>";
|
||||
}, "");
|
||||
}
|
||||
|
||||
function show_new() {
|
||||
view_timeline_show_new(DAMUS);
|
||||
}
|
||||
|
||||
function click_share(el) {
|
||||
share(el.dataset.evid);
|
||||
}
|
||||
|
||||
function click_toggle_like(el) {
|
||||
// Disable the button to prevent multiple presses
|
||||
el.disabled = true;
|
||||
if (el.dataset.liked == "yes") {
|
||||
delete_post(el.dataset.reactionId);
|
||||
return;
|
||||
}
|
||||
send_reply(R_SHAKA, el.dataset.reactingTo);
|
||||
}
|
||||
|
||||
/* open_media_preview presents a modal to display an image via "url".
|
||||
*/
|
||||
function open_media_preview(url, type) {
|
||||
const el = find_node("#media-preview");
|
||||
el.showModal();
|
||||
find_node("img", el).src = url;
|
||||
// TODO handle different medias such as audio and video
|
||||
// TODO add loading state & error checking
|
||||
}
|
||||
|
||||
/* close_media_preview closes any present media modal.
|
||||
*/
|
||||
function close_media_preview() {
|
||||
find_node("#media-preview").close();
|
||||
}
|
||||
|
||||
function delete_post_confirm(evid) {
|
||||
if (!confirm("Are you sure you want to delete this post?"))
|
||||
return;
|
||||
const reason = (prompt("Why you are deleting this? Leave empty to not specify. Type CANCEL to cancel.") || "").trim()
|
||||
if (reason.toLowerCase() === "cancel")
|
||||
return;
|
||||
delete_post(evid, reason)
|
||||
}
|
||||
|
||||
async function do_send_reply(all=false) {
|
||||
const modal = document.querySelector("#reply-modal");
|
||||
const replying_to = modal.querySelector("#replying-to");
|
||||
const evid = replying_to.dataset.evid;
|
||||
const reply_content_el = document.querySelector("#reply-content");
|
||||
const content = reply_content_el.value;
|
||||
await send_reply(content, evid, all);
|
||||
reply_content_el.value = "";
|
||||
close_modal(modal);
|
||||
}
|
||||
|
||||
function update_reply_box(state="new") {
|
||||
const isnew = state == "new";
|
||||
const modal = document.querySelector("#reply-modal");
|
||||
modal.querySelector("#replying-to").classList.toggle("hide", isnew);
|
||||
modal.querySelector("header label").textContent = isnew ? "New Note" : "Replying To";
|
||||
modal.querySelector(".post-tools.new").classList.toggle("hide", !isnew);
|
||||
modal.querySelector(".post-tools.reply").classList.toggle("hide", isnew);
|
||||
}
|
||||
|
||||
function new_note() {
|
||||
const modal = document.querySelector("#reply-modal");
|
||||
const inputbox = modal.querySelector("#reply-content");
|
||||
update_reply_box("new");
|
||||
inputbox.placeholder = "What's up?";
|
||||
modal.showModal();
|
||||
inputbox.focus();
|
||||
}
|
||||
|
||||
function reply(evid) {
|
||||
const ev = DAMUS.all_events[evid]
|
||||
const modal = document.querySelector("#reply-modal")
|
||||
const replybox = modal.querySelector("#reply-content")
|
||||
const replying_to = modal.querySelector("#replying-to")
|
||||
update_reply_box("reply");
|
||||
replying_to.dataset.evid = evid
|
||||
replying_to.classList.remove("hide");
|
||||
replying_to.innerHTML = render_event_nointeract(DAMUS, ev, {
|
||||
is_composing: true,
|
||||
nobar: true
|
||||
});
|
||||
replybox.placeholder = "Reply...";
|
||||
modal.showModal();
|
||||
replybox.focus();
|
||||
}
|
||||
|
||||
function update_favicon(path) {
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
const head = document.getElementsByTagName('head')[0]
|
||||
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
||||
link.href = path;
|
||||
}
|
||||
|
||||
// update_notifications updates the document title & visual indicators based on if the
|
||||
// number of notifications that are unseen by the user.
|
||||
function update_notifications(model) {
|
||||
let dm_count = 0;
|
||||
for (const item of model.dms) {
|
||||
if (item[1].new_count)
|
||||
dm_count += item[1].new_count;
|
||||
}
|
||||
|
||||
const { count } = model.notifications;
|
||||
const suffix = "Yo Sup";
|
||||
const total = count + dm_count;
|
||||
document.title = total ? `(${total}) ${suffix}` : suffix;
|
||||
// TODO I broke the favicons; I will fix with later.
|
||||
//update_favicon(has_notes ? "img/damus_notif.svg" : "img/damus.svg");
|
||||
update_notification_markers(count, "activity");
|
||||
update_notification_markers(dm_count, "dm");
|
||||
// slight hack :)
|
||||
find_node("#header-tools button[action='mark-all-read']")
|
||||
.disabled = dm_count == 0;
|
||||
}
|
||||
|
||||
async function get_pubkey(use_prompt=true) {
|
||||
if (!(window.nostr && window.nostr.getPublicKey)) {
|
||||
console.error("window.nostr.getPublicKey is unsupported");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await window.nostr.getPublicKey()
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function open_thread(thread_id) {
|
||||
view_timeline_apply_mode(DAMUS, VM_THREAD, { thread_id });
|
||||
}
|
||||
|
||||
function close_modal(el) {
|
||||
find_parent(el, "dialog").close();
|
||||
}
|
||||
|
||||
function on_click_show_event_details(evid) {
|
||||
const model = DAMUS;
|
||||
const ev = model.all_events[evid];
|
||||
if (!ev)
|
||||
return;
|
||||
const el = find_node("#event-details");
|
||||
el.showModal();
|
||||
find_node("code", el).innerText = JSON.stringify(ev, null, "\t");
|
||||
}
|
||||
|
||||
function onclick_pfp(ev) {
|
||||
open_profile(ev.target.dataset.pubkey);
|
||||
}
|
||||
|
||||
function onerror_pfp(ev) {
|
||||
ev.target.src = IMG_NO_USER;
|
||||
}
|
168
key/bech32.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
var ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
||||
var ALPHABET_MAP = {};
|
||||
for (var z = 0; z < ALPHABET.length; z++) {
|
||||
var x = ALPHABET.charAt(z);
|
||||
ALPHABET_MAP[x] = z;
|
||||
}
|
||||
function polymodStep(pre) {
|
||||
var b = pre >> 25;
|
||||
return (((pre & 0x1ffffff) << 5) ^
|
||||
(-((b >> 0) & 1) & 0x3b6a57b2) ^
|
||||
(-((b >> 1) & 1) & 0x26508e6d) ^
|
||||
(-((b >> 2) & 1) & 0x1ea119fa) ^
|
||||
(-((b >> 3) & 1) & 0x3d4233dd) ^
|
||||
(-((b >> 4) & 1) & 0x2a1462b3));
|
||||
}
|
||||
function prefixChk(prefix) {
|
||||
var chk = 1;
|
||||
for (var i = 0; i < prefix.length; ++i) {
|
||||
var c = prefix.charCodeAt(i);
|
||||
if (c < 33 || c > 126)
|
||||
return 'Invalid prefix (' + prefix + ')';
|
||||
chk = polymodStep(chk) ^ (c >> 5);
|
||||
}
|
||||
chk = polymodStep(chk);
|
||||
for (var i = 0; i < prefix.length; ++i) {
|
||||
var v = prefix.charCodeAt(i);
|
||||
chk = polymodStep(chk) ^ (v & 0x1f);
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
function convertbits(data, inBits, outBits, pad) {
|
||||
var value = 0;
|
||||
var bits = 0;
|
||||
var maxV = (1 << outBits) - 1;
|
||||
var result = [];
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
value = (value << inBits) | data[i];
|
||||
bits += inBits;
|
||||
while (bits >= outBits) {
|
||||
bits -= outBits;
|
||||
result.push((value >> bits) & maxV);
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
result.push((value << (outBits - bits)) & maxV);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (bits >= inBits)
|
||||
return 'Excess padding';
|
||||
if ((value << (outBits - bits)) & maxV)
|
||||
return 'Non-zero padding';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function toWords(bytes) {
|
||||
return convertbits(bytes, 8, 5, true);
|
||||
}
|
||||
function fromWordsUnsafe(words) {
|
||||
var res = convertbits(words, 5, 8, false);
|
||||
if (Array.isArray(res))
|
||||
return res;
|
||||
}
|
||||
function fromWords(words) {
|
||||
var res = convertbits(words, 5, 8, false);
|
||||
if (Array.isArray(res))
|
||||
return res;
|
||||
throw new Error(res);
|
||||
}
|
||||
function getLibraryFromEncoding(encoding) {
|
||||
var ENCODING_CONST;
|
||||
if (encoding === 'bech32') {
|
||||
ENCODING_CONST = 1;
|
||||
}
|
||||
else {
|
||||
ENCODING_CONST = 0x2bc830a3;
|
||||
}
|
||||
function encode(prefix, words, LIMIT) {
|
||||
LIMIT = LIMIT || 90;
|
||||
if (prefix.length + 7 + words.length > LIMIT)
|
||||
throw new TypeError('Exceeds length limit');
|
||||
prefix = prefix.toLowerCase();
|
||||
// determine chk mod
|
||||
var chk = prefixChk(prefix);
|
||||
if (typeof chk === 'string')
|
||||
throw new Error(chk);
|
||||
var result = prefix + '1';
|
||||
for (var i = 0; i < words.length; ++i) {
|
||||
var x = words[i];
|
||||
if (x >> 5 !== 0)
|
||||
throw new Error('Non 5-bit word');
|
||||
chk = polymodStep(chk) ^ x;
|
||||
result += ALPHABET.charAt(x);
|
||||
}
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
chk = polymodStep(chk);
|
||||
}
|
||||
chk ^= ENCODING_CONST;
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
var v = (chk >> ((5 - i) * 5)) & 0x1f;
|
||||
result += ALPHABET.charAt(v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function __decode(str, LIMIT) {
|
||||
LIMIT = LIMIT || 90;
|
||||
if (str.length < 8)
|
||||
return str + ' too short';
|
||||
if (str.length > LIMIT)
|
||||
return 'Exceeds length limit';
|
||||
// don't allow mixed case
|
||||
var lowered = str.toLowerCase();
|
||||
var uppered = str.toUpperCase();
|
||||
if (str !== lowered && str !== uppered)
|
||||
return 'Mixed-case string ' + str;
|
||||
str = lowered;
|
||||
var split = str.lastIndexOf('1');
|
||||
if (split === -1)
|
||||
return 'No separator character for ' + str;
|
||||
if (split === 0)
|
||||
return 'Missing prefix for ' + str;
|
||||
var prefix = str.slice(0, split);
|
||||
var wordChars = str.slice(split + 1);
|
||||
if (wordChars.length < 6)
|
||||
return 'Data too short';
|
||||
var chk = prefixChk(prefix);
|
||||
if (typeof chk === 'string')
|
||||
return chk;
|
||||
var words = [];
|
||||
for (var i = 0; i < wordChars.length; ++i) {
|
||||
var c = wordChars.charAt(i);
|
||||
var v = ALPHABET_MAP[c];
|
||||
if (v === undefined)
|
||||
return 'Unknown character ' + c;
|
||||
chk = polymodStep(chk) ^ v;
|
||||
// not in the checksum?
|
||||
if (i + 6 >= wordChars.length)
|
||||
continue;
|
||||
words.push(v);
|
||||
}
|
||||
if (chk !== ENCODING_CONST)
|
||||
return 'Invalid checksum for ' + str;
|
||||
return { prefix: prefix, words: words };
|
||||
}
|
||||
function decodeUnsafe(str, LIMIT) {
|
||||
var res = __decode(str, LIMIT);
|
||||
if (typeof res === 'object')
|
||||
return res;
|
||||
}
|
||||
function decode(str, LIMIT) {
|
||||
var res = __decode(str, LIMIT);
|
||||
if (typeof res === 'object')
|
||||
return res;
|
||||
throw new Error(res);
|
||||
}
|
||||
return {
|
||||
decodeUnsafe: decodeUnsafe,
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
toWords: toWords,
|
||||
fromWordsUnsafe: fromWordsUnsafe,
|
||||
fromWords: fromWords
|
||||
};
|
||||
}
|
||||
|
||||
const bech32 = getLibraryFromEncoding('bech32');
|
||||
const bech32m = getLibraryFromEncoding('bech32m');
|
50
key/index.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>damus key converter</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="Damus">
|
||||
<meta property="og:description" content="A new social network that you control">
|
||||
<meta property="og:image" content="https://damus.io/img/logo.png">
|
||||
<meta property="og:url" content="https://damus.io">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
|
||||
<link rel="stylesheet" href="/css/normalize.css">
|
||||
<link rel="stylesheet" href="/css/skeleton.css?v=3">
|
||||
<link rel="stylesheet" href="/css/custom.css?v=4">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
|
||||
<span class="damus">damus</span>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<h1>Key Converter</h2>
|
||||
<p>Convert a damus key to an old-style hex key</p>
|
||||
<label for="bech32">damus key</label>
|
||||
<input type="text" class="u-full-width" placeholder="npub... OR nsec..." id="damus-key">
|
||||
|
||||
<label for="bech32">hex key</label>
|
||||
<input type="text" class="u-full-width" placeholder="" id="hex-key">
|
||||
|
||||
<div>
|
||||
<h2>Links</h2>
|
||||
<a id="note-link" href="">Damus Note Link</a><br/>
|
||||
<a id="profile-link" href="">Damus Profile Link</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="bech32.js" ></script>
|
||||
<script src="key.js?v=3" ></script>
|
||||
</body>
|
||||
</html>
|
44
key/key.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
|
||||
function hex_char(val)
|
||||
{
|
||||
if (val < 10)
|
||||
return String.fromCharCode(48 + val)
|
||||
if (val < 16)
|
||||
return String.fromCharCode(97 + val - 10)
|
||||
}
|
||||
|
||||
function hex_encode(buf)
|
||||
{
|
||||
str = ""
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
const c = buf[i]
|
||||
str += hex_char(c >> 4)
|
||||
str += hex_char(c & 0xF)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function go() {
|
||||
const el = document.querySelector("#damus-key")
|
||||
const hex_el = document.querySelector("#hex-key")
|
||||
const note_link_el = document.querySelector("#note-link")
|
||||
const profile_link_el = document.querySelector("#profile-link")
|
||||
|
||||
el.addEventListener("input", () => {
|
||||
const decoded = bech32.decode(el.value)
|
||||
const bytes = fromWords(decoded.words)
|
||||
hex_el.value = hex_encode(bytes)
|
||||
update_note_link(hex_el.value)
|
||||
});
|
||||
|
||||
hex_el.addEventListener("input", () => {
|
||||
update_note_link(hex_el.value)
|
||||
})
|
||||
|
||||
function update_note_link(id) {
|
||||
note_link_el.href = `nostr:e:${id}`
|
||||
profile_link_el.href = `nostr:p:${id}`
|
||||
}
|
||||
}
|
||||
|
||||
go()
|
1
log/.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
export PATH=$PWD:$PATH
|
16
log/2022-08-02-introducing-damus-log.gmi
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
# The Damus Log - Powered by #nostr
|
||||
|
||||
Hey there, Welcome to the damus log! A blog powered by... nostr! What does this mean!? What is nostr? Let's find out!
|
||||
|
||||
nostr is what powers damus, an iOS nostr client we're working on. It's a fancy pants new internet protocol designed to be the email of social networks. Imagine if email was controlled by a single company. Everyone would have to use the same email client (probably something like gmail), and a single company would have complete control over all your data... everyone's data!
|
||||
|
||||
This isn't good, this is why the internet today was originally built on these decentralized protocols. Things like websites and email are all available on different platforms, clients and servers. This freedom to pick and choose prevents any single company to have complete control over our data.
|
||||
|
||||
nostr is an attempt to do the same for social networks themselves. It provides a censorship resistant, real-time database. Anyone can run a nostr relay and no single relay is in control of the data. It's quite ingenious if we say so ourselves.
|
||||
|
||||
We like it so much we've made our blog nostr-powered! The comments below are from the nostr network. You can comment on it from the damus client itself! If you're interested in trying it out, try out the testflight at the bottom of our homepage:
|
||||
|
||||
=> https://damus.io damus.io
|
||||
|
||||
Looking forward to seeing you on nostr!
|
69
log/2022-08-02-introducing-damus-log.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>The Damus Log</title>
|
||||
<link rel="stylesheet" href="log.css?v=29">
|
||||
<link rel="stylesheet" href="comments.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<a href="https://damus.io/log" class="date">< The Damus Log</a>
|
||||
<h1 id="the-damus-log---powered-by-nostr">The Damus Log - Powered by
|
||||
#nostr</h1>
|
||||
<p>Hey there, Welcome to the damus log! A blog powered by… nostr! What
|
||||
does this mean!? What is nostr? Let’s find out!</p>
|
||||
<p>nostr is what powers damus, an iOS nostr client we’re working on.
|
||||
It’s a fancy pants new internet protocol designed to be the email of
|
||||
social networks. Imagine if email was controlled by a single company.
|
||||
Everyone would have to use the same email client (probably something
|
||||
like gmail), and a single company would have complete control over all
|
||||
your data… everyone’s data!</p>
|
||||
<p>This isn’t good, this is why the internet today was originally built
|
||||
on these decentralized protocols. Things like websites and email are all
|
||||
available on different platforms, clients and servers. This freedom to
|
||||
pick and choose prevents any single company to have complete control
|
||||
over our data.</p>
|
||||
<p>nostr is an attempt to do the same for social networks themselves. It
|
||||
provides a censorship resistant, real-time database. Anyone can run a
|
||||
nostr relay and no single relay is in control of the data. It’s quite
|
||||
ingenious if we say so ourselves.</p>
|
||||
<p>We like it so much we’ve made our blog nostr-powered! The comments
|
||||
below are from the nostr network. You can comment on it from the damus
|
||||
client itself! If you’re interested in trying it out, try out the
|
||||
testflight at the bottom of our homepage:</p>
|
||||
<p><a href="https://damus.io">damus.io</a></p>
|
||||
<p>Looking forward to seeing you on nostr!</p>
|
||||
|
||||
<h3><a id="comment-link" href="nostr:e:">Comments</a></h3>
|
||||
<div id="comments">
|
||||
</div>
|
||||
<script src="nostr.js?v=4" ></script>
|
||||
<script src="comments.js?v=16" ></script>
|
||||
<script>
|
||||
const threads = {
|
||||
"the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
|
||||
"introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
|
||||
"stop-developing": "7328ba525fba9240a13dc67bc8ccf9248d90fa0c752419ccbe0558e129ea96d5",
|
||||
}
|
||||
let relay
|
||||
for (const key of Object.keys(threads)) {
|
||||
if (window.location.href.includes(key)) {
|
||||
const id = threads[key]
|
||||
relay = comments_init(id)
|
||||
document.querySelector("#comment-link").href = 'nostr:e:' + id
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
51
log/2022-08-19-the-stuff-loads-better-release.gmi
Normal file
|
@ -0,0 +1,51 @@
|
|||
|
||||
|
||||
# v0.1.3 - The "Stuff Loads Better" Release
|
||||
|
||||
It's that time again! A new damus release. This one fixes a bunch of annoying issues such as profiles not loading properly in some situations. We also do a much better job at caching profile pictures, so no more weird poppyness and wasting your cell data.
|
||||
|
||||
If you're not on the testflight already, you can get it here:
|
||||
|
||||
=> https://testflight.apple.com/join/CLwjLxWl Damus TestFlight
|
||||
|
||||
This was the last release before lightning support, so next version will be exciting!!
|
||||
|
||||
Anyways, here's the full changlog!
|
||||
|
||||
```
|
||||
# Added
|
||||
|
||||
- Support kind 42 chat messages (ArcadeCity).
|
||||
- Friend icons next to names on some views. Check is friend. Arrows are friend-of-friends
|
||||
- Load chat view first if content contains #chat
|
||||
- Cancel button on search box
|
||||
- Added profile picture cache
|
||||
- Multiline DM messages
|
||||
|
||||
# Changed
|
||||
|
||||
- #hashtags now use the `t` tag instead of `hashtag`
|
||||
- Clicking a chatroom quote reply will now expand it instead of jumping to it
|
||||
- Clicking on a note will now always scroll it to the bottom
|
||||
- Check note ids and signatures on every note
|
||||
- use bech32 ids everywhere
|
||||
- Don't animate scroll in chat view
|
||||
- Post button is not shown if the content is only whitespace
|
||||
|
||||
# Fixed
|
||||
|
||||
- Fixed thread loading issue when clicking on boosts
|
||||
- Fixed various issues with chatroom view
|
||||
- Fix bug where sometimes nested navigation views weren't dismissed when tapping the tab bar
|
||||
- Fixed minor carousel spacing issue on homescreen
|
||||
- You can now reference users, notes hashtags in DMs
|
||||
- Profile pics are now loaded in the background
|
||||
- Limit post sizes to max 32,000 as an upper bound sanity limit.
|
||||
- Missing profiles are now loaded everywhere
|
||||
- No longer parse hashtags in urls
|
||||
- Logging out now resets your keypair and actually logs out
|
||||
- Copying text in DMs will now copy the decrypted text
|
||||
```
|
||||
|
||||
=> https://damus.io Damus TestFlight
|
||||
|
89
log/2022-08-19-the-stuff-loads-better-release.html
Normal file
|
@ -0,0 +1,89 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>The Damus Log</title>
|
||||
<link rel="stylesheet" href="log.css?v=29">
|
||||
<link rel="stylesheet" href="comments.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<a href="https://damus.io/log" class="date">< The Damus Log</a>
|
||||
<h1 id="v0.1.3---the-stuff-loads-better-release">v0.1.3 - The “Stuff
|
||||
Loads Better” Release</h1>
|
||||
<p>It’s that time again! A new damus release. This one fixes a bunch of
|
||||
annoying issues such as profiles not loading properly in some
|
||||
situations. We also do a much better job at caching profile pictures, so
|
||||
no more weird poppyness and wasting your cell data.</p>
|
||||
<p>If you’re not on the testflight already, you can get it here:</p>
|
||||
<p><a href="https://testflight.apple.com/join/CLwjLxWl">Damus
|
||||
TestFlight</a></p>
|
||||
<p>This was the last release before lightning support, so next version
|
||||
will be exciting!!</p>
|
||||
<p>Anyways, here’s the full changlog!</p>
|
||||
<pre><code># Added
|
||||
|
||||
- Support kind 42 chat messages (ArcadeCity).
|
||||
- Friend icons next to names on some views. Check is friend. Arrows are friend-of-friends
|
||||
- Load chat view first if content contains #chat
|
||||
- Cancel button on search box
|
||||
- Added profile picture cache
|
||||
- Multiline DM messages
|
||||
|
||||
# Changed
|
||||
|
||||
- #hashtags now use the `t` tag instead of `hashtag`
|
||||
- Clicking a chatroom quote reply will now expand it instead of jumping to it
|
||||
- Clicking on a note will now always scroll it to the bottom
|
||||
- Check note ids and signatures on every note
|
||||
- use bech32 ids everywhere
|
||||
- Don't animate scroll in chat view
|
||||
- Post button is not shown if the content is only whitespace
|
||||
|
||||
# Fixed
|
||||
|
||||
- Fixed thread loading issue when clicking on boosts
|
||||
- Fixed various issues with chatroom view
|
||||
- Fix bug where sometimes nested navigation views weren't dismissed when tapping the tab bar
|
||||
- Fixed minor carousel spacing issue on homescreen
|
||||
- You can now reference users, notes hashtags in DMs
|
||||
- Profile pics are now loaded in the background
|
||||
- Limit post sizes to max 32,000 as an upper bound sanity limit.
|
||||
- Missing profiles are now loaded everywhere
|
||||
- No longer parse hashtags in urls
|
||||
- Logging out now resets your keypair and actually logs out
|
||||
- Copying text in DMs will now copy the decrypted text</code></pre>
|
||||
<p><a href="https://damus.io">Damus TestFlight</a></p>
|
||||
|
||||
<h3><a id="comment-link" href="nostr:e:">Comments</a></h3>
|
||||
<div id="comments">
|
||||
</div>
|
||||
<script src="nostr.js?v=4" ></script>
|
||||
<script src="comments.js?v=16" ></script>
|
||||
<script>
|
||||
const threads = {
|
||||
"the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
|
||||
"introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
|
||||
"stop-developing": "7328ba525fba9240a13dc67bc8ccf9248d90fa0c752419ccbe0558e129ea96d5",
|
||||
}
|
||||
let relay
|
||||
for (const key of Object.keys(threads)) {
|
||||
if (window.location.href.includes(key)) {
|
||||
const id = threads[key]
|
||||
relay = comments_init(id)
|
||||
document.querySelector("#comment-link").href = 'nostr:e:' + id
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
17
log/2022-09-16-ok-can-you-guys-stop-developing-release.gmi
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
# v0.1.4 - Ok can you guys stop developing
|
||||
|
||||
So someone had the smart idea[1] to create nostr chatrooms, so naturally damus should support these. I know I said the previous release would be the last release before lightning support, but chatrooms are pretty cool and I wanted to squeeze in an initial chatroom release. So here we are.
|
||||
|
||||
I haven't added full chatroom support yet, like searching for and joining channels. The way that this release currently works is that if you see one of your friends chatting in a chatroom, you'll be able to reply to that chat from your home feed, or pop into the chatroom by clicking on the post.
|
||||
|
||||
In the future I plan on having full chatroom support, probably something like how telegram displays their chatrooms.
|
||||
|
||||
It's up on testflight now so check it out!
|
||||
|
||||
Enjoy!
|
||||
|
||||
=> https://anigma.io [1] Anigma
|
||||
|
||||
=> https://testflight.apple.com/join/CLwjLxWl Damus TestFlight
|
||||
|
63
log/2022-09-16-ok-can-you-guys-stop-developing-release.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>The Damus Log</title>
|
||||
<link rel="stylesheet" href="log.css?v=29">
|
||||
<link rel="stylesheet" href="comments.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<a href="https://damus.io/log" class="date">< The Damus Log</a>
|
||||
<h1 id="v0.1.4---ok-can-you-guys-stop-developing">v0.1.4 - Ok can you
|
||||
guys stop developing</h1>
|
||||
<p>So someone had the smart idea[1] to create nostr chatrooms, so
|
||||
naturally damus should support these. I know I said the previous release
|
||||
would be the last release before lightning support, but chatrooms are
|
||||
pretty cool and I wanted to squeeze in an initial chatroom release. So
|
||||
here we are.</p>
|
||||
<p>I haven’t added full chatroom support yet, like searching for and
|
||||
joining channels. The way that this release currently works is that if
|
||||
you see one of your friends chatting in a chatroom, you’ll be able to
|
||||
reply to that chat from your home feed, or pop into the chatroom by
|
||||
clicking on the post.</p>
|
||||
<p>In the future I plan on having full chatroom support, probably
|
||||
something like how telegram displays their chatrooms.</p>
|
||||
<p>It’s up on testflight now so check it out!</p>
|
||||
<p>Enjoy!</p>
|
||||
<p><a href="https://anigma.io">[1] Anigma</a></p>
|
||||
<p><a href="https://testflight.apple.com/join/CLwjLxWl">Damus
|
||||
TestFlight</a></p>
|
||||
|
||||
<h3><a id="comment-link" href="nostr:e:">Comments</a></h3>
|
||||
<div id="comments">
|
||||
</div>
|
||||
<script src="nostr.js?v=4" ></script>
|
||||
<script src="comments.js?v=16" ></script>
|
||||
<script>
|
||||
const threads = {
|
||||
"the-stuff-loads-better-release": "9941b55c2844f275b7b8714a1c39859088a425ce798f740ea8fea879f9098641",
|
||||
"introducing-damus-log": "4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371",
|
||||
"stop-developing": "7328ba525fba9240a13dc67bc8ccf9248d90fa0c752419ccbe0558e129ea96d5",
|
||||
}
|
||||
let relay
|
||||
for (const key of Object.keys(threads)) {
|
||||
if (window.location.href.includes(key)) {
|
||||
const id = threads[key]
|
||||
relay = comments_init(id)
|
||||
document.querySelector("#comment-link").href = 'nostr:e:' + id
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
17
log/Makefile
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
POSTS=$(wildcard *.gmi)
|
||||
HTMLS=$(POSTS:.gmi=.html)
|
||||
|
||||
|
||||
all: $(HTMLS)
|
||||
|
||||
clean: fake
|
||||
rm -f $(HTMLS)
|
||||
|
||||
dist: all
|
||||
rsync -avzP ./ charon:/www/damus.io/log/
|
||||
|
||||
%.html: %.gmi head.html tail.html
|
||||
./gmi2md < $< | pandoc -f markdown -t html -o - | cat head.html - tail.html > $@
|
||||
|
||||
.PHONY: fake
|
69
log/comments.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
.pfp {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 15px 0 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
font-family: system-ui, sans;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment p {
|
||||
background-color: rgba(255.0,255.0,255.0,0.1);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.comment .info {
|
||||
text-align: right;
|
||||
width: 18%;
|
||||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
.quote {
|
||||
border-left: 2px solid white;
|
||||
margin-left: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255.0,255.0,255.0,0.1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comment .info span {
|
||||
font-size: 11px;
|
||||
color: rgba(255.0,255.0,255.0,0.7);
|
||||
}
|
||||
|
||||
@media (max-width: 800px){
|
||||
/* Reverse the order of elements in the user comments,
|
||||
so that the avatar and info appear after the text. */
|
||||
.comment .info {
|
||||
order: 2;
|
||||
width: 50%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pfp {
|
||||
order: 1;
|
||||
margin: 0 15px 0 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255.0,255.0,255.0,0.1);
|
||||
}
|
||||
|
||||
.comment p {
|
||||
order: 3;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
177
log/comments.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function comments_init(thread)
|
||||
{
|
||||
const relay = await Relay("wss://relay.damus.io")
|
||||
const now = (new Date().getTime()) / 1000
|
||||
const model = {events: [], profiles: {}}
|
||||
const comments_id = uuidv4()
|
||||
const profiles_id = uuidv4()
|
||||
|
||||
model.pool = relay
|
||||
model.el = document.querySelector("#comments")
|
||||
|
||||
relay.subscribe(comments_id, {kinds: [1], "#e": [thread]})
|
||||
|
||||
relay.event = (sub_id, ev) => {
|
||||
if (sub_id === comments_id) {
|
||||
if (ev.content !== "")
|
||||
insert_event_sorted(model.events, ev)
|
||||
if (model.realtime)
|
||||
render_home_view(model)
|
||||
} else if (sub_id === profiles_id) {
|
||||
try {
|
||||
model.profiles[ev.pubkey] = JSON.parse(ev.content)
|
||||
} catch {
|
||||
console.log("failed to parse", ev.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relay.eose = async (sub_id) => {
|
||||
if (sub_id === comments_id) {
|
||||
handle_comments_loaded(profiles_id, model)
|
||||
} else if (sub_id === profiles_id) {
|
||||
handle_profiles_loaded(profiles_id, model)
|
||||
}
|
||||
}
|
||||
|
||||
return relay
|
||||
}
|
||||
|
||||
function handle_profiles_loaded(profiles_id, model) {
|
||||
// stop asking for profiles
|
||||
model.pool.unsubscribe(profiles_id)
|
||||
model.realtime = true
|
||||
render_home_view(model)
|
||||
}
|
||||
|
||||
// load profiles after comment notes are loaded
|
||||
function handle_comments_loaded(profiles_id, model)
|
||||
{
|
||||
const pubkeys = model.events.reduce((s, ev) => {
|
||||
s.add(ev.pubkey)
|
||||
return s
|
||||
}, new Set())
|
||||
const authors = Array.from(pubkeys)
|
||||
|
||||
// load profiles
|
||||
model.pool.subscribe(profiles_id, {kinds: [0], authors: authors})
|
||||
}
|
||||
|
||||
function render_home_view(model) {
|
||||
model.el.innerHTML = render_events(model)
|
||||
}
|
||||
|
||||
function render_events(model) {
|
||||
const render = render_event.bind(null, model)
|
||||
return model.events.map(render).join("\n")
|
||||
}
|
||||
|
||||
function render_event(model, ev) {
|
||||
const profile = model.profiles[ev.pubkey] || {
|
||||
name: "anon",
|
||||
display_name: "Anonymous",
|
||||
}
|
||||
const delta = time_delta(new Date().getTime(), ev.created_at*1000)
|
||||
return `
|
||||
<div class="comment">
|
||||
<div class="info">
|
||||
${render_name(ev.pubkey, profile)}
|
||||
<span>${delta}</span>
|
||||
</div>
|
||||
<img class="pfp" src="${get_picture(ev.pubkey, profile)}">
|
||||
<p>
|
||||
${format_content(ev.content)}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function convert_quote_blocks(content)
|
||||
{
|
||||
const split = content.split("\n")
|
||||
let blockin = false
|
||||
return split.reduce((str, line) => {
|
||||
if (line !== "" && line[0] === '>') {
|
||||
if (!blockin) {
|
||||
str += "<span class='quote'>"
|
||||
blockin = true
|
||||
}
|
||||
str += sanitize(line.slice(1))
|
||||
} else {
|
||||
if (blockin) {
|
||||
blockin = false
|
||||
str += "</span>"
|
||||
}
|
||||
str += sanitize(line)
|
||||
}
|
||||
return str + "<br/>"
|
||||
}, "")
|
||||
}
|
||||
|
||||
function format_content(content)
|
||||
{
|
||||
return convert_quote_blocks(content)
|
||||
}
|
||||
|
||||
function sanitize(content)
|
||||
{
|
||||
if (!content)
|
||||
return ""
|
||||
return content.replaceAll("<","<").replaceAll(">",">")
|
||||
}
|
||||
|
||||
function get_picture(pk, profile)
|
||||
{
|
||||
return sanitize(profile.picture) || "https://robohash.org/" + pk
|
||||
}
|
||||
|
||||
function render_name(pk, profile={})
|
||||
{
|
||||
const display_name = profile.display_name || profile.user
|
||||
const username = profile.name || "anon"
|
||||
const name = display_name || username
|
||||
|
||||
return `<div class="username">${sanitize(name)}</div>`
|
||||
}
|
||||
|
||||
function time_delta(current, previous) {
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
|
||||
var elapsed = current - previous;
|
||||
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed/1000) + ' seconds ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed/msPerMinute) + ' minutes ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerDay ) {
|
||||
return Math.round(elapsed/msPerHour ) + ' hours ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed/msPerDay) + ' days ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed/msPerMonth) + ' months ago';
|
||||
}
|
||||
|
||||
else {
|
||||
return Math.round(elapsed/msPerYear ) + ' years ago';
|
||||
}
|
||||
}
|
25
log/gmi2md
Executable file
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env sedef
|
||||
|
||||
# gmi2md: Sed script to convert text/gemini to markdown.
|
||||
# Based on v0.14.2 of the gemini spec.
|
||||
#
|
||||
# This script is dedicated to the public domain according to the terms of CC0:
|
||||
# https://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
x
|
||||
/^```/ {
|
||||
x
|
||||
/^```/ {
|
||||
x
|
||||
s/.*//
|
||||
x
|
||||
}
|
||||
b
|
||||
}
|
||||
g
|
||||
|
||||
/^=>/ {
|
||||
s/[][()]/\\&/g
|
||||
s/^=>\s*([^[:space:]]+)\s*$/[\1](\1)/
|
||||
s/^=>\s*([^[:space:]]+)\s+(.+)/[\2](\1)/
|
||||
}
|
19
log/head.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>The Damus Log</title>
|
||||
<link rel="stylesheet" href="log.css?v=29">
|
||||
<link rel="stylesheet" href="comments.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<a href="https://damus.io/log" class="date">< The Damus Log</a>
|
1
log/img
Symbolic link
|
@ -0,0 +1 @@
|
|||
../img/
|
37
log/index.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>The Damus Log</title>
|
||||
<link rel="stylesheet" href="log.css?v=28">
|
||||
<link rel="stylesheet" href="comments.css?v=5">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="/img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
|
||||
<h1>The Damus Log</h1>
|
||||
<ul>
|
||||
<li><a href="2022-09-16-ok-can-you-guys-stop-developing-release.html">v0.1.4 - Ok can you guys stop developing</a><span class="date">2022-09-16</span></li>
|
||||
<li><a href="2022-08-19-the-stuff-loads-better-release.html">v0.1.3 - The "Stuff Loads Better" Release</a><span class="date">2022-08-19</span></li>
|
||||
<li><a href="2022-08-02-introducing-damus-log.html">Introducing The Damus Log</a><span class="date">2022-08-02</span></li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h3><a href="nostr:e:2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423">Comments</a></h3>
|
||||
<div id="comments">
|
||||
</div>
|
||||
<script src="nostr.js?v=4" ></script>
|
||||
<script src="comments.js?v=16" ></script>
|
||||
<script>
|
||||
const relay = comments_init("2ed9b99190f0acf8f5cf768d4edd4be004a1262c6d296f341333e5e94b5ec423")
|
||||
</script>
|
||||
</div> <!-- container -->
|
||||
</body>
|
||||
</html>
|
167
log/log.css
Normal file
|
@ -0,0 +1,167 @@
|
|||
@import url('https://rsms.me/inter/inter.css');
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 50px 0 0 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 0;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.7em;
|
||||
margin-left: 10px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
padding-right: 18px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: -system-ui, sans-serif;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body {
|
||||
color: white;
|
||||
min-height: 800px;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
font-size: 20px;
|
||||
font-family: "Georgia", sans-serif;
|
||||
|
||||
background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, rgba(255,11,214,1) 100%);
|
||||
}
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 36em;
|
||||
hyphens: auto;
|
||||
word-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
font-size: 0.9em;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
.container {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
font-size: 12pt;
|
||||
}
|
||||
p, h2, h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
h2, h3, h4 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
margin-top: 1.4em;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 1.7em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
li > ol, li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0 1em 1.7em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #e6e6e6;
|
||||
color: #606060;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
.sourceCode {
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
hr {
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
table {
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
}
|
||||
table caption {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
tbody {
|
||||
margin-top: 0.5em;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
th {
|
||||
border-top: 1px solid #1a1a1a;
|
||||
padding: 0.25em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
td {
|
||||
padding: 0.125em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
#TOC li {
|
||||
list-style: none;
|
||||
}
|
||||
#TOC a:not(:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
code{white-space: pre-wrap;}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
span.underline{text-decoration: underline;}
|
||||
div.column{display: inline-block; vertical-align: top; width: 50%;}
|
||||
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||
ul.task-list{list-style: none;}
|
||||
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
73
log/nostr.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
|
||||
function insert_event_sorted(evs, new_ev) {
|
||||
for (let i = 0; i < evs.length; i++) {
|
||||
const ev = evs[i]
|
||||
|
||||
if (new_ev.id === ev.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (new_ev.created_at > ev.created_at) {
|
||||
evs.splice(i, 0, new_ev)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
evs.push(new_ev)
|
||||
return true
|
||||
}
|
||||
|
||||
function Relay(relay, opts={})
|
||||
{
|
||||
if (!(this instanceof Relay))
|
||||
return new Relay(relay, opts)
|
||||
|
||||
this.relay = relay
|
||||
this.opts = opts
|
||||
|
||||
const me = this
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = me.ws = new WebSocket(relay);
|
||||
let resolved = false
|
||||
ws.onmessage = (m) => { handle_nostr_message(me, m) }
|
||||
ws.onclose = () => { me.close && me.close() }
|
||||
ws.onerror = () => { me.error && me.error() }
|
||||
ws.onopen = () => {
|
||||
if (resolved) {
|
||||
me.open.bind(me)
|
||||
return
|
||||
}
|
||||
|
||||
resolved = true
|
||||
resolve(me)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Relay.prototype.subscribe = function relay_subscribe(sub_id, ...filters) {
|
||||
const tosend = ["REQ", sub_id, ...filters]
|
||||
this.ws.send(JSON.stringify(tosend))
|
||||
}
|
||||
|
||||
Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) {
|
||||
const tosend = ["CLOSE", sub_id]
|
||||
this.ws.send(JSON.stringify(tosend))
|
||||
}
|
||||
|
||||
function handle_nostr_message(relay, msg)
|
||||
{
|
||||
const data = JSON.parse(msg.data)
|
||||
if (data.length >= 2) {
|
||||
switch (data[0]) {
|
||||
case "EVENT":
|
||||
if (data.length < 3)
|
||||
return
|
||||
return relay.event && relay.event(data[1], data[2])
|
||||
case "EOSE":
|
||||
return relay.eose && relay.eose(data[1])
|
||||
case "NOTICE":
|
||||
return relay.note && relay.note(...data.slice(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|