patchboot
Version:
A Scuttlebutt bootloader
293 lines (261 loc) • 9.17 kB
JavaScript
import { VotesManager } from '../VotesManager.js';
import { IdentityManager } from '../IdentityManager.js';
class AppController extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const appDescription = this.msg.value;
const votesManager = new VotesManager(this.sbot)
const controllerArea = this.attachShadow({ mode: 'open' });
controllerArea.innerHTML = `
<style>
* {
box-sizing: border-box;
}
#app {
font-size: 16px;
font-family: Inter, 'Helvetica Neue', Arial, Helvetica, sans-serif;
border-bottom: 1px solid gray;
width: 100%;
margin: 0;
padding: 8px 6px 0 8px;
}
.bar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
padding-bottom: 8px;
white-space: nowrap;
}
.info > * {
text-overflow: ellipsis;
overflow-x: hidden;
}
.details {
padding-bottom: 0;
overflow-x: hidden;
height: 0;
white-space: normal;
}
#app.expanded .info * {
height: unset;
white-space: normal;
}
#app.expanded .details {
padding-bottom: 8px;
height: auto;
}
.name {
font-size: 17px;
line-height: 20px;
height: 20px;
font-weight: 600;
}
#author,
.time,
#author-id {
font-size: 13px;
line-height: 16px;
height: 16px;
}
.time:not(:empty)::before {
content: 'Published on ';
color: gray;
}
#author-id {
font-family: monospace;
color: gray;
}
.details .actions {
float: right;
}
.actions {
display: flex;
margin-left: 6px;
}
.actions button {
margin: 0 2px;
padding: 6px;
border: none;
border-radius: 50%;
height: 36px;
background-color: #f8f8f8;
}
.actions button:hover {
background-color: #dddddd;
}
.actions button svg {
display: block;
height: 24px;
width: 24px;
}
.svghover .onhover {
display: none;
}
.svghover:hover path {
display: none;
}
.svghover:hover .onhover {
display: unset;
}
.hidden {
display: none;
}
.count {
position: relative;
}
.count[data-count]::before {
content: attr(data-count);
position: absolute;
bottom: 0;
left: 0;
font-size: 13px;
line-height: 13px;
font-weight: 600;
padding: 0 0 4px 4px;
border-top-right-radius: 4px;
/* color: #ff2f92; */
background-color: #f8f8f8;
background: linear-gradient(45deg, rgba(255,255,255,0) 0%, #f8f8f8 100%);
}
.count[data-count]:hover::before {
background: linear-gradient(45deg, rgba(255, 255, 255, 0) 50%, rgba(221, 221, 221, 1) 100%);
}
</style>
<div id="app">
<div class="bar">
<div class="info">
<div class="name">${appDescription.content.name || appDescription.content.mentions[0].name || appDescription.content.comment || ''}</div>
<div id="author"></div>
</div>
<div class="actions">
<button title="Like" id="like" class="svghover hidden count">
<svg width="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" />
<path class="onhover" fill="currentColor"
d="M12.67 20.74L12 21.35L10.55 20.03C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3C9.24 3 10.91 3.81 12 5.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5C22 9.93 21.5 11.26 20.62 12.61C20 12.31 19.31 12.11 18.59 12.04C19.5 10.8 20 9.65 20 8.5C20 6.5 18.5 5 16.5 5C14.96 5 13.46 6 12.93 7.36H11.07C10.54 6 9.04 5 7.5 5C5.5 5 4 6.5 4 8.5C4 11.39 7.14 14.24 11.89 18.55L12 18.65L12.04 18.61C12.12 19.37 12.34 20.09 12.67 20.74M17 14V17H14V19H17V22H19V19H22V17H19V14H17Z" />
</svg>
</button>
<button title="Unlike" id="unlike" class="svghover hidden count">
<svg width="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
<path class="onhover" fill="currentColor"
d="M12 18C12 19 12.25 19.92 12.67 20.74L12 21.35L10.55 20.03C5.4 15.36 2 12.27 2 8.5C2 5.41 4.42 3 7.5 3C9.24 3 10.91 3.81 12 5.08C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.41 22 8.5C22 9.93 21.5 11.26 20.62 12.61C19.83 12.23 18.94 12 18 12C14.69 12 12 14.69 12 18M14 17V19H22V17H14Z" />
</svg>
</button>
<button title="Run" id="run">
<svg width="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
</svg>
</button>
</div>
</div>
<div class="details bar">
<div class="info">
<div class="actions">
<button title="Revoke App" id="revoke" class="hidden">
<svg width="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41" />
</svg>
</button>
<button title="View Source" id="source">
<svg width="24" viewBox="0 0 24 24">
<path fill="currentColor"
d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" />
</svg>
</button>
</div>
<div class="comment">${appDescription.content.comment || ''}</div>
<div id="author-id">${appDescription.author}</div>
<div class="time">${(new Date(appDescription.timestamp)).toLocaleString() || ''}</div>
</div>
</div>
</div>`
const appEl = controllerArea.getElementById('app')
appEl.addEventListener('click', e => {
appEl.classList.toggle('expanded')
})
const renderLikesStuff = () => {
votesManager.getVotes(this.msg.key).then(likes => {
const count = likes.length
if (count > 0) {
controllerArea.querySelectorAll('.count').forEach(e => e.setAttribute('data-count', count))
} else {
controllerArea.querySelectorAll('.count').forEach(e => e.removeAttribute('data-count'))
}
})
votesManager.getOwnVote(this.msg.key).then(liked => {
if (liked) {
this.classList.add('liked')
controllerArea.getElementById('like').classList.add('hidden')
controllerArea.getElementById('unlike').classList.remove('hidden')
} else {
this.classList.remove('liked')
controllerArea.getElementById('like').classList.remove('hidden')
controllerArea.getElementById('unlike').classList.add('hidden')
}
}, e => {
console.log("error getting own vote:", e)
})
}
renderLikesStuff()
this.sbot.whoami().then(
currentUser => this.msg.value.author === currentUser.id)
.then(own => {
if (own) {
controllerArea.getElementById('revoke').classList.remove('hidden')
}
})
;(new IdentityManager(this.sbot)).getSelfAssignedName(appDescription.author).then(name => {
controllerArea.getElementById('author').innerHTML = name
}).catch(e => console.log(e));
controllerArea.getElementById('run').addEventListener('click', e => {
e.stopPropagation()
this.dispatchEvent(new Event('run'));
})
controllerArea.getRootNode().getElementById('source').addEventListener('click', e => {
e.stopPropagation()
this.dispatchEvent(new Event('view-source'));
})
controllerArea.getRootNode().getElementById('like').addEventListener('click', async e => {
e.stopPropagation()
await votesManager.vote(this.msg.key, 1)
renderLikesStuff()
this.dispatchEvent(new Event('like'))
})
controllerArea.getRootNode().getElementById('unlike').addEventListener('click', async e => {
e.stopPropagation()
await votesManager.vote(this.msg.key, 0)
renderLikesStuff()
this.dispatchEvent(new Event('unlike'))
})
controllerArea.getRootNode().getElementById('revoke').addEventListener('click', async e => {
e.stopPropagation()
if (!confirm("Revoke App? This action cannot be undone.")) {
return;
}
await this.revoke()
this.parentElement.removeChild(this)
this.dispatchEvent(new Event('revoked'))
})
}
revoke() {
return new Promise((resolve, reject) => {
this.sbot.publish({
type: 'about',
about: this.msg.key,
status: 'revoked'
}, function (err, msg) {
if (err) {
reject(err)
} else {
resolve(true)
}
})
})
}
}
customElements.define("app-controller", AppController);