diff --git a/dist/index.css b/dist/index.css index 8811f7c..f58da7f 100644 --- a/dist/index.css +++ b/dist/index.css @@ -26,6 +26,30 @@ body { height: 100%; } +.avatarcontainer { + height: 30px; + padding: 3px; +} + +.avatarlink { + cursor: pointer; + position: absolute; +} + +.avatarlink > .img { + width: 30px; + height: 30px; + padding-right: 5px; +} + +.avatarlink > .name { + position: absolute; + top: 5px; + display: block; + height: 30px; + left: 35px; +} + a { color: #000; text-decoration: underline; @@ -102,8 +126,9 @@ ul.dashed { flex-grow: 4; } -#profile > span > .description h2 { - margin-top: 0; +#profile > span > .description > .feedId { + margin-bottom: 0.5rem; + font-size: .8rem; } #profile > h2 { diff --git a/dist/index.html b/dist/index.html index 91fe813..5df00ce 100644 --- a/dist/index.html +++ b/dist/index.html @@ -7,9 +7,9 @@
diff --git a/dist/noavatar.svg b/dist/noavatar.svg new file mode 100644 index 0000000..c198ed7 --- /dev/null +++ b/dist/noavatar.svg @@ -0,0 +1,20 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + \ No newline at end of file diff --git a/index.js b/index.js index f980107..ed6e54d 100644 --- a/index.js +++ b/index.js @@ -68,6 +68,9 @@ function getGroupKeysFeed(SSB, cb) { }) } +// load ssb-profile-link +require('./ssb-profile-link') + const menu = new Vue({ el: '#menu', @@ -277,14 +280,6 @@ function setupApp(SSB) { } }, 1000) - // main feed replicated on rpc connect - SSB.on('rpc:connect', function (rpc, isClient) { - if (rpc.id !== roomKey) { - console.log("request connect", rpc.id) - SSB.ebt.request(rpc.id, true) - } - }) - // find all meta feeds & children and replicate those pull( SSB.db.query( diff --git a/profile.js b/profile.js new file mode 100644 index 0000000..88ebe1b --- /dev/null +++ b/profile.js @@ -0,0 +1,444 @@ +module.exports = function (feedId) { + const ssbSingleton = require('ssb-browser-core/ssb-singleton') + const mnemonic = require('ssb-keys-mnemonic') + + let initialState = function(self) { + return { + componentStillLoaded: false, + isSelf: false, + following: false, + blocking: false, + name: '', + image: 'noavatar.svg', + imageBlobId: '', + showFriends: true, + showBlocked: false, + showFollowers: false, + showBlockingUs: false, + friends: [], + followers: [], + blocked: [], + blockingUs: [], + waitingForBlobURLs: 0, + + showExportKey: false, + showImportKey: false, + mnemonic: '', + } + } + + return { + template: ` +
+
+ +
+
+
+
+
+
{{ feedId }}
+ Name: +
+
+ +
+
+ 🚫 +
+
+
{{ feedId }}
+

{{ name }}

+
+
+
+ +
+ + +
+
+ + +
+

+
+

▼► Following ({{ friends.length }})

+
+
+ +
+
+
+

▼► Blocking ({{ blocked.length }})

+
+
+ +
+
+
+

▼► Followers ({{ followers.length }})

+
+
+ +
+
+
+

▼► Blocking us ({{ blockingUs.length }})

+
+
+ +
+
+
+ + + + + + + + +
+
`, + + data: function() { + return initialState(this) + }, + + computed: { + followText: function() { return this.following ? 'Unfollow' : 'Follow' }, + blockText: function() { return this.blocking ? 'Unblock' : 'Block' }, + }, + + methods: { + cacheImageURLForPreview: function(blobId, cb) { + var self = this; + [ err, SSB ] = ssbSingleton.getSSB() + if (!SSB || !SSB.blobs) { + // Not going to hurt anything to try again later. + setTimeout(function() { + self.cacheImageURLForPreview(blobId, cb) + }, 3000) + return + } + + ++this.waitingForBlobURLs + SSB.blobs.fsURL(blobId, (err, blobURL) => { + // If this is the last blob we were waiting for, call the callback. + --self.waitingForBlobURLs + if (self.waitingForBlobURLs == 0) + cb(null, true) + }) + }, + + onFileSelect: function(ev) { + const file = ev.target.files[0] + + if (!file) return + + var self = this; + [ err, SSB ] = ssbSingleton.getSSB() + if (!SSB || !SSB.blobs) { + alert("Can't add file right now. Database couldn't be locked. Please make sure you only have one running instance of ssb-browser.") + return + } + + file.arrayBuffer().then(function (buffer) { + SSB.blobs.hash(new Uint8Array(buffer), (err, digest) => { + var blobId = "&" + digest + SSB.blobs.add(blobId, file, (err) => { + if (!err) { + SSB.blobs.push(blobId, (err) => { + SSB.blobs.localGet(blobId, (err, url) => { + if (!err) { + self.image = url + self.imageBlobId = blobId + } + }) + }) + } else + alert("failed to add img", err) + }) + }) + }) + }, + + exportKey: function() { + this.mnemonic = mnemonic.keysToWords(JSON.parse(localStorage["/.groupies/secret"])) + this.showExportKey = true + }, + + restoreKey: function() { + const key = mnemonic.wordsToKeys(this.mnemonic) + localStorage["/.groupies/secret"] = JSON.stringify(key) + this.showImportKey = false + + localStorage["/.groupies/restoreFeed"] = "true" + + alert("Please reload") + }, + + saveProfile: function() { + [ err, SSB ] = ssbSingleton.getSSB() + if (!SSB || !SSB.db) { + alert("Can't save right now. Database couldn't be locked. Please make sure you only have one running instance of ssb-browser.") + return + } + + var msg = { type: 'about', about: SSB.id } + if (this.name) + msg.name = this.name + if (this.imageBlobId != '') { + msg.image = { + link: this.imageBlobId + } + } + + // Make sure the full post (including headers) is not larger than the 8KiB limit. + if (JSON.stringify(msg).length > 8192) { + alert('Your post is too large. Each post can only be 8KiB. Please shorten your post or split it into multiple posts.') + return + } + + SSB.db.publish(msg, (err) => { + if (err) return alert(err) + + alert("Saved!") + }) + }, + + changeFollowStatus: function() { + [ err, SSB ] = ssbSingleton.getSSB() + var self = this + if (!SSB || !SSB.db) { + alert("Can't change follow status right now. Database couldn't be locked. Please make sure you only have one running instance of ssb-browser.") + return + } + + if (this.following) { + SSB.db.publish({ + type: 'contact', + contact: this.feedId, + following: false + }, () => { + alert('Unfollowed') // FIXME: proper UI + }) + } else { + SSB.db.publish({ + type: 'contact', + contact: this.feedId, + following: true + }, () => { + alert('Followed') // FIXME: proper UI + }) + } + }, + + changeBlockStatus: function() { + [ err, SSB ] = ssbSingleton.getSSB() + if (!SSB || !SSB.db) { + alert("Can't change blocking status right now. Database couldn't be locked. Please make sure you only have one running instance of ssb-browser.") + return + } + + var self = this + if (this.blocking) { + SSB.db.publish({ + type: 'contact', + contact: this.feedId, + blocking: false + }, () => { + self.blocking = false + alert('Unblocked') // FIXME: proper UI + }) + } else { + SSB.db.publish({ + type: 'contact', + contact: this.feedId, + blocking: true + }, () => { + SSB.db.deleteFeed(this.feedId, (err) => { + if (err) { + self.blocking = true + alert('Failed to delete messages, but user is blocked.') + } else { + alert('Blocked') // FIXME: proper UI + } + }) + }) + } + }, + + deleteFeed: function() { + [ err, SSB ] = ssbSingleton.getSSB() + if (!SSB || !SSB.db) { + alert("Can't delete feed right now. Database couldn't be locked. Please make sure you only have one running instance of ssb-browser.") + return + } + + var self = this + SSB.db.deleteFeed(this.feedId, (err) => { + if (err) return alert('Failed to remove feed', err) + + this.$router.push({ path: '/public'}) + }) + }, + + updateFollowers: function(err, SSB) { + var self = this + var opts = { + start: this.feedId, + max: 1, + reverse: true + } + SSB.friends.hops(opts, (err, feeds) => { + var newFollowers = [] + for(f in feeds) { + if (feeds[f] > 0) + newFollowers.push(f) + } + self.followers = newFollowers + }) + }, + + updateBlockingUs: function(err, SSB) { + var self = this + var opts = { + start: this.feedId, + max: 0, + reverse: true + } + SSB.friends.hops(opts, (err, feeds) => { + var newBlocks = [] + for(f in feeds) { + if (Math.round(feeds[f]) == -1) + newBlocks.push(f) + } + self.blockingUs = newBlocks + }) + }, + + renderFollowsCallback: function (err, SSB) { + var self = this + + self.isSelf = (SSB.id == this.feedId) + + SSB.helpers.getGraphForFeed(self.feedId, (err, graph) => { + self.friends = graph.following + self.blocked = graph.blocking + + SSB.friends.isFollowing({ source: SSB.id, dest: self.feedId }, (err, result) => { + self.following = result + }) + + SSB.friends.isBlocking({ source: SSB.id, dest: self.feedId }, (err, result) => { + self.blocking = result + }) + }) + + this.updateFollowers(err, SSB) + this.updateBlockingUs(err, SSB) + }, + + renderProfileCallback: function (err, SSB) { + var self = this + const profile = SSB.db.getIndex("aboutSelf").getProfile(self.feedId) + if (profile.name) + self.name = profile.name + + if (profile.imageURL) { + self.image = profile.imageURL + self.imageBlobId = profile.image + } else if (profile.image) { + SSB.blobs.localGet(profile.image, (err, url) => { + if (!err) { + self.image = url + self.imageBlobId = profile.image + } + }) + } + }, + + renderProfile() { + var self = this + + ssbSingleton.getSSBEventually( + -1, + () => { return self.componentStillLoaded }, + (SSB) => { return SSB && SSB.db }, + self.renderFollowsCallback + ) + + ssbSingleton.getSSBEventually( + -1, + () => { return self.componentStillLoaded }, + (SSB) => { + if (!SSB || !SSB.db) return false + + let profile = SSB.db.getIndex("aboutSelf").getProfile(self.feedId) + return Object.keys(profile).length > 0 + }, + self.renderProfileCallback + ) + } + }, + + created: function () { + this.feedId = feedId + Object.assign(this.$data, initialState(this)) + this.componentStillLoaded = true + + this.renderProfile() + }, + + destroyed: function () { + this.componentStillLoaded = false + } + } +} diff --git a/ssb-profile-link.js b/ssb-profile-link.js new file mode 100644 index 0000000..45c67cb --- /dev/null +++ b/ssb-profile-link.js @@ -0,0 +1,110 @@ +const ssbSingleton = require('ssb-browser-core/ssb-singleton') + +Vue.component('ssb-profile-link', { + template: ` +
+ +
`, + + props: ['feed'], + + data: function() { + return { + componentStillLoaded: false, + imgURL: '', + isBlocked: false, + name: '' + } + }, + + methods: { + renderProfile: function(profile) { + var self = this + ssbSingleton.getSSBEventually( + -1, + () => { return self.componentStillLoaded }, + (SSB) => { return SSB }, + (err, SSB) => { + self.renderProfileCallback(err, SSB, profile) + } + ) + }, + + renderProfileCallback: function (err, SSB, existingProfile) { + const self = this + const profile = existingProfile || SSB.db.getIndex("aboutSelf").getProfile(self.feed) + + // set a default image to be overridden if there is an actual avatar to show. + self.imgURL = "noavatar.svg" + + if (self.feed == SSB.id) + self.name = 'You' + else + self.name = profile.name + + if (profile.imageURL) self.imgURL = profile.imageURL + else if (profile.image) { + SSB.blobs.localProfileGet(profile.image, (err, url) => { + if (err) return console.error("failed to get img", err) + + profile.imageURL = self.imgURL = url + }) + } + }, + + openProfile: function() { + const profile = require('./profile') + new Vue(profile(this.feed)).$mount("#app") + }, + + loadBlocking: function (err, SSB) { + SSB.friends.isBlocking({ source: SSB.id, dest: self.feed }, (err, result) => { + if (!err) self.isBlocked = result + }) + }, + + refresh: function() { + var self = this + + // set a default image while we wait for an SSB. + self.imgURL = "noavatar.svg" + + ssbSingleton.getSSBEventually( + -1, + () => { return self.componentStillLoaded }, + (SSB) => { return SSB }, + self.loadBlocking + ) + + ssbSingleton.getSSBEventually( + -1, + () => { return self.componentStillLoaded }, + (SSB) => { + if (!SSB || !SSB.db) return false + + let profile = SSB.db.getIndex("aboutSelf").getProfile(self.feed) + return Object.keys(profile).length > 0 + }, + self.renderProfileCallback) + } + }, + + created: function() { + this.componentStillLoaded = true + this.refresh() + }, + + destroyed: function() { + this.componentStillLoaded = false + }, + + watch: { + feed: function (oldValue, newValue) { + this.refresh() + } + } +})