From 091ff04c2420c8549ff581399212b4e3346864ee Mon Sep 17 00:00:00 2001 From: Anthony Wang Date: Wed, 15 Feb 2023 16:22:43 -0500 Subject: Delete all files except for demo --- README.md | 5 - components/chat.js | 65 +++++++ components/comment.js | 71 +++++++ components/like-button.js | 36 ++++ components/moderation.js | 63 +++++++ components/name.js | 56 ++++++ components/private-messaging.js | 66 +++++++ demo/components/chat.js | 65 ------- demo/components/comment.js | 71 ------- demo/components/like-button.js | 36 ---- demo/components/moderation.js | 63 ------- demo/components/name.js | 56 ------ demo/components/private-messaging.js | 66 ------- demo/index.html | 73 -------- demo/style.css | 3 - graffiti.js | 351 ----------------------------------- index.html | 73 ++++++++ plugins/vue/plugin.js | 76 -------- src/array.js | 61 ------ src/auth.js | 125 ------------- src/logoot.js | 153 --------------- style.css | 3 + 22 files changed, 433 insertions(+), 1204 deletions(-) delete mode 100644 README.md create mode 100644 components/chat.js create mode 100644 components/comment.js create mode 100644 components/like-button.js create mode 100644 components/moderation.js create mode 100644 components/name.js create mode 100644 components/private-messaging.js delete mode 100644 demo/components/chat.js delete mode 100644 demo/components/comment.js delete mode 100644 demo/components/like-button.js delete mode 100644 demo/components/moderation.js delete mode 100644 demo/components/name.js delete mode 100644 demo/components/private-messaging.js delete mode 100644 demo/index.html delete mode 100644 demo/style.css delete mode 100644 graffiti.js create mode 100644 index.html delete mode 100644 plugins/vue/plugin.js delete mode 100644 src/array.js delete mode 100644 src/auth.js delete mode 100644 src/logoot.js create mode 100644 style.css diff --git a/README.md b/README.md deleted file mode 100644 index 05412b9..0000000 --- a/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Graffiti Javascript Library - -This library enables any webpage to interface with the [Graffiti server](https://github.com/graffiti-garden/graffiti-server). It also includes a plugin that extends it to operate with the [Vue.js framework](https://vuejs.org/). - -Check out the live [demo](https://graffiti.garden/graffiti-js/demo) of the library and plugin in action. The demo's source code is in the [`/demo`](https://github.com/graffiti-garden/graffiti-js/tree/main/demo) folder. diff --git a/components/chat.js b/components/chat.js new file mode 100644 index 0000000..5095dcd --- /dev/null +++ b/components/chat.js @@ -0,0 +1,65 @@ +import { Name } from './name.js' +import Comments from './comment.js' + +export default { + + components: { Name, Comments }, + + data: ()=> ({ + message: '', + channel: 'demo' + }), + + methods: { + messageObjects(objects) { + return objects.filter(o=> + 'message' in o && + 'timestamp' in o && + typeof o.message == 'string' && + typeof o.timestamp == 'number') + .sortBy('timestamp') + }, + + sendMessage() { + if (!this.message) return + this.$graffitiUpdate({ + message: this.message, + timestamp: Date.now(), + _tags: [this.channel] + }) + this.message = '' + } + }, + + template: ` +

+ Chat Channel: +

+ + + + + +
+ + +
` +} + diff --git a/components/comment.js b/components/comment.js new file mode 100644 index 0000000..d838ee1 --- /dev/null +++ b/components/comment.js @@ -0,0 +1,71 @@ +import LikeButton from './like-button.js' + +export default { + name: 'Comments', + + components: { LikeButton }, + + props: ['messageID'], + + methods: { + commentObjects(objects) { + console.log(111111111111) + console.log(objects.filter(o=>'like' in o)) + console.log(objects.filter(o=>'comment' in o)) + return objects.filter(o=> + 'comment' in o && + 'timestamp' in o && + o.comment == this.messageID && + typeof o.timestamp == 'number') + .sort((a,b)=> + objects.filter(o=> + 'like' in o && + o.like == a._id).length + < objects.filter(o=> + 'like' in o && + o.like == b._id).length + ) + }, + + sendComment(objects) { + this.$graffitiUpdate({ + comment: this.messageID, + timestamp: Date.now(), + message: this.message, + _tags: [this.messageID] + }) + } + }, + + template: ` + +
+ Comment +
+ + +
+
+
+ Collapse thread + +
+
` +} + diff --git a/components/like-button.js b/components/like-button.js new file mode 100644 index 0000000..6f65ac6 --- /dev/null +++ b/components/like-button.js @@ -0,0 +1,36 @@ +export default { + + props: ['messageID', 'parent'], + + methods: { + likeObjects(objects, messageID=this.messageID) { + return objects.filter(o=> + 'like' in o && + 'timestamp' in o && + o.like == messageID && + typeof o.timestamp == 'number') + + }, + + toggleLike(objects) { + const myLikes = this.likeObjects(objects).mine + if (myLikes.length) { + myLikes.removeMine() + } else { + this.$graffitiUpdate({ + like: this.messageID, + timestamp: Date.now(), + _tags: [this.messageID, this.parent] + }) + } + } + }, + + template: ` + + + ` +} + diff --git a/components/moderation.js b/components/moderation.js new file mode 100644 index 0000000..5e12fb0 --- /dev/null +++ b/components/moderation.js @@ -0,0 +1,63 @@ +import Chat from './chat.js' +import LikeButton from './like-button.js' +import { Name } from './name.js' + +export default { + + data: ()=> ({ + likeThreshold: 0, + channel: 'demo', + admin: null + }), + + methods: { + messageObjects: Chat.methods.messageObjects, + likeObjects: LikeButton.methods.likeObjects, + }, + + template: ` +

+ Chat Channel: +

+ + + +

Example 1

+ +

+ Only show me objects with more than likes. +

+ + + +

Example 2

+ +

+ Only show me objects that + + has liked. +

+ + + +
` +} + diff --git a/components/name.js b/components/name.js new file mode 100644 index 0000000..ea44f95 --- /dev/null +++ b/components/name.js @@ -0,0 +1,56 @@ +export const Name = { + + props: ["of"], + + methods: { + name(objects) { + const nameObjects = objects + .filter(o=> + 'name' in o && + 'of' in o && + 'timestamp' in o && + typeof o.name == 'string' && + o.of == this.of && + o._by == this.of && + typeof o.timestamp == 'number') + .sortBy('-timestamp') + + return nameObjects.length? + nameObjects[0].name : 'anonymous' + } + }, + + template: ` + + {{ name(objects) }} + ` +} + +export const SetMyName = { + + props: ["tags"], + + data: ()=> ({ + name: '' + }), + + methods: { + setMyName() { + this.$graffitiUpdate({ + name: this.name, + timestamp: Date.now(), + of: this.$graffitiMyID, + _tags: this.tags + }) + this.name = '' + } + }, + + template: ` +
+ + +
+ +
` +} diff --git a/components/private-messaging.js b/components/private-messaging.js new file mode 100644 index 0000000..61f783b --- /dev/null +++ b/components/private-messaging.js @@ -0,0 +1,66 @@ +import Chat from './chat.js' +import {Name} from './name.js' + +export default { + + data: ()=> ({ + recipient: null, + message: '' + }), + + methods: { + messageObjects: Chat.methods.messageObjects, + chatObjects(objects) { + return this.messageObjects(objects).filter(o=> + '_to' in o && o._to.length == 1) + }, + + sendMessage() { + if (!this.message) return + this.$graffitiUpdate({ + message: this.message, + timestamp: Date.now(), + _to: [this.recipient], + _tags: [this.$graffitiMyID, this.recipient] + }) + this.message = '' + } + }, + + template: ` + Send private message to: + + + + +
+ + +
+ + + +

My Outbox

+ + + +

My Inbox

+ + + +
` +} diff --git a/demo/components/chat.js b/demo/components/chat.js deleted file mode 100644 index 5095dcd..0000000 --- a/demo/components/chat.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Name } from './name.js' -import Comments from './comment.js' - -export default { - - components: { Name, Comments }, - - data: ()=> ({ - message: '', - channel: 'demo' - }), - - methods: { - messageObjects(objects) { - return objects.filter(o=> - 'message' in o && - 'timestamp' in o && - typeof o.message == 'string' && - typeof o.timestamp == 'number') - .sortBy('timestamp') - }, - - sendMessage() { - if (!this.message) return - this.$graffitiUpdate({ - message: this.message, - timestamp: Date.now(), - _tags: [this.channel] - }) - this.message = '' - } - }, - - template: ` -

- Chat Channel: -

- - - - - -
- - -
` -} - diff --git a/demo/components/comment.js b/demo/components/comment.js deleted file mode 100644 index d838ee1..0000000 --- a/demo/components/comment.js +++ /dev/null @@ -1,71 +0,0 @@ -import LikeButton from './like-button.js' - -export default { - name: 'Comments', - - components: { LikeButton }, - - props: ['messageID'], - - methods: { - commentObjects(objects) { - console.log(111111111111) - console.log(objects.filter(o=>'like' in o)) - console.log(objects.filter(o=>'comment' in o)) - return objects.filter(o=> - 'comment' in o && - 'timestamp' in o && - o.comment == this.messageID && - typeof o.timestamp == 'number') - .sort((a,b)=> - objects.filter(o=> - 'like' in o && - o.like == a._id).length - < objects.filter(o=> - 'like' in o && - o.like == b._id).length - ) - }, - - sendComment(objects) { - this.$graffitiUpdate({ - comment: this.messageID, - timestamp: Date.now(), - message: this.message, - _tags: [this.messageID] - }) - } - }, - - template: ` - -
- Comment -
- - -
-
-
- Collapse thread - -
-
` -} - diff --git a/demo/components/like-button.js b/demo/components/like-button.js deleted file mode 100644 index 6f65ac6..0000000 --- a/demo/components/like-button.js +++ /dev/null @@ -1,36 +0,0 @@ -export default { - - props: ['messageID', 'parent'], - - methods: { - likeObjects(objects, messageID=this.messageID) { - return objects.filter(o=> - 'like' in o && - 'timestamp' in o && - o.like == messageID && - typeof o.timestamp == 'number') - - }, - - toggleLike(objects) { - const myLikes = this.likeObjects(objects).mine - if (myLikes.length) { - myLikes.removeMine() - } else { - this.$graffitiUpdate({ - like: this.messageID, - timestamp: Date.now(), - _tags: [this.messageID, this.parent] - }) - } - } - }, - - template: ` - - - ` -} - diff --git a/demo/components/moderation.js b/demo/components/moderation.js deleted file mode 100644 index 5e12fb0..0000000 --- a/demo/components/moderation.js +++ /dev/null @@ -1,63 +0,0 @@ -import Chat from './chat.js' -import LikeButton from './like-button.js' -import { Name } from './name.js' - -export default { - - data: ()=> ({ - likeThreshold: 0, - channel: 'demo', - admin: null - }), - - methods: { - messageObjects: Chat.methods.messageObjects, - likeObjects: LikeButton.methods.likeObjects, - }, - - template: ` -

- Chat Channel: -

- - - -

Example 1

- -

- Only show me objects with more than likes. -

- - - -

Example 2

- -

- Only show me objects that - - has liked. -

- - - -
` -} - diff --git a/demo/components/name.js b/demo/components/name.js deleted file mode 100644 index ea44f95..0000000 --- a/demo/components/name.js +++ /dev/null @@ -1,56 +0,0 @@ -export const Name = { - - props: ["of"], - - methods: { - name(objects) { - const nameObjects = objects - .filter(o=> - 'name' in o && - 'of' in o && - 'timestamp' in o && - typeof o.name == 'string' && - o.of == this.of && - o._by == this.of && - typeof o.timestamp == 'number') - .sortBy('-timestamp') - - return nameObjects.length? - nameObjects[0].name : 'anonymous' - } - }, - - template: ` - - {{ name(objects) }} - ` -} - -export const SetMyName = { - - props: ["tags"], - - data: ()=> ({ - name: '' - }), - - methods: { - setMyName() { - this.$graffitiUpdate({ - name: this.name, - timestamp: Date.now(), - of: this.$graffitiMyID, - _tags: this.tags - }) - this.name = '' - } - }, - - template: ` -
- - -
- -
` -} diff --git a/demo/components/private-messaging.js b/demo/components/private-messaging.js deleted file mode 100644 index 61f783b..0000000 --- a/demo/components/private-messaging.js +++ /dev/null @@ -1,66 +0,0 @@ -import Chat from './chat.js' -import {Name} from './name.js' - -export default { - - data: ()=> ({ - recipient: null, - message: '' - }), - - methods: { - messageObjects: Chat.methods.messageObjects, - chatObjects(objects) { - return this.messageObjects(objects).filter(o=> - '_to' in o && o._to.length == 1) - }, - - sendMessage() { - if (!this.message) return - this.$graffitiUpdate({ - message: this.message, - timestamp: Date.now(), - _to: [this.recipient], - _tags: [this.$graffitiMyID, this.recipient] - }) - this.message = '' - } - }, - - template: ` - Send private message to: - - - - -
- - -
- - - -

My Outbox

- - - -

My Inbox

- - - -
` -} diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index d1b5991..0000000 --- a/demo/index.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - Graffiti JS Demo - - - - - - - - - -

Graffiti Demo

- -

Connection Status

- -

- Connected to the Graffiti server? {{ $graffitiConnected }} -

- -

Logging In

- -

- -

- -

- My Graffiti ID is {{ $graffitiMyID }} -

- -

Profile

- -

- My name is: -

- - - -

Chatting

- - - -

Moderation

- - - -

Private Messaging

- - - - diff --git a/demo/style.css b/demo/style.css deleted file mode 100644 index 7bdf592..0000000 --- a/demo/style.css +++ /dev/null @@ -1,3 +0,0 @@ -h2 { - margin-top: 1.5em; -} diff --git a/graffiti.js b/graffiti.js deleted file mode 100644 index 65c6ece..0000000 --- a/graffiti.js +++ /dev/null @@ -1,351 +0,0 @@ -import Auth from './src/auth.js' -import GraffitiArray from './src/array.js' - -export default class { - - // There needs to be a new object map for each tag - constructor( - graffitiURL="https://graffiti.garden", - objectConstructor=()=>({})) { - - this.graffitiURL = graffitiURL - this.open = false - this.eventTarget = new EventTarget() - this.tagMap = objectConstructor() // tag->{count, Set(uuid)} - this.objectMap = objectConstructor() // uuid->object - this.GraffitiArray = GraffitiArray(this) - - this.#initialize() - } - - async #initialize() { - // Perform authorization - this.authParams = await Auth.connect(this.graffitiURL) - - // Rewrite the URL - this.wsURL = new URL(this.graffitiURL) - this.wsURL.host = "app." + this.wsURL.host - if (this.wsURL.protocol == 'https:') { - this.wsURL.protocol = 'wss:' - } else { - this.wsURL.protocol = 'ws:' - } - if (this.authParams.token) { - this.wsURL.searchParams.set("token", this.authParams.token) - } - - // Commence connection - this.#connect() - } - - // Wait for the connection to be - // open (state=true) or closed (state=false) - async connectionState(state) { - if (this.open != state) { - await new Promise(resolve => { - this.eventTarget.addEventListener( - state? "open": "closed", ()=> resolve()) - }) - } - } - - #connect() { - this.ws = new WebSocket(this.wsURL) - this.ws.onmessage = this.#onMessage.bind(this) - this.ws.onclose = this.#onClose.bind(this) - this.ws.onopen = this.#onOpen.bind(this) - } - - // authorization functions - get myID() { return this.authParams.myID } - toggleLogIn() { - this.myID? Auth.logOut() : Auth.logIn(this.graffitiURL) - } - - async #onClose() { - console.error("lost connection to graffiti server, attemping reconnect soon...") - this.open = false - this.eventTarget.dispatchEvent(new Event("closed")) - await new Promise(resolve => setTimeout(resolve, 2000)) - this.#connect() - } - - async #request(msg) { - if (!this.open) { - throw "Can't make request! Not connected to graffiti server" - } - - // Create a random message ID - const messageID = crypto.randomUUID() - - // Create a listener for the reply - const dataPromise = new Promise(resolve => { - this.eventTarget.addEventListener('$'+messageID, (e) => { - resolve(e.data) - }) - }) - - // Send the request - msg.messageID = messageID - this.ws.send(JSON.stringify(msg)) - - // Await the reply - const data = await dataPromise - delete data.messageID - - if ('error' in data) { - throw data - } else { - return data.reply - } - } - - #onMessage(event) { - const data = JSON.parse(event.data) - - if ('messageID' in data) { - // It's a reply - // Forward it back to the sender - const messageEvent = new Event('$'+data.messageID) - messageEvent.data = data - this.eventTarget.dispatchEvent(messageEvent) - - } else if ('update' in data) { - this.#updateCallback(data['update']) - - } else if ('remove' in data) { - this.#removeCallback(data['remove']) - - } else if (data.type == 'error') { - if (data.reason == 'authorization') { - Auth.logOut() - } - throw data - } - } - - #updateCallback(object) { - const uuid = this.#objectUUID(object) - - // Add the UUID to the tag map - let subscribed = false - for (const tag of object._tags) { - if (!(tag in this.tagMap)) continue - this.tagMap[tag].uuids.add(uuid) - subscribed = true - } - - if (!subscribed) return - - // Define object specific properties - if (!('_id' in object)) { - // Assign the object UUID - Object.defineProperty(object, '_id', { value: uuid }) - - // Add proxy functions so object modifications - // sync with the server - object = new Proxy(object, this.#objectHandler(object, true)) - } - - this.objectMap[uuid] = object - } - - #removeCallback(object) { - const uuid = this.#objectUUID(object) - - // Remove the UUID from all relevant tag maps - let supported = false - for (const tag in this.tagMap) { - if (this.tagMap[tag].uuids.has(uuid)) { - if (object._tags.includes(tag)) { - this.tagMap[tag].uuids.delete(uuid) - } else { - supported = true - } - } - } - - // If all tags have been removed, delete entirely - if (!supported && uuid in this.objectMap) { - delete this.objectMap[uuid] - } - } - - async update(object) { - object._by = this.myID - if (!object._key) object._key = crypto.randomUUID() - - // Immediately replace the object - this.#updateCallback(object) - - // Send it to the server - try { - await this.#request({ update: object }) - } catch(e) { - // Delete the temp object - this.#removeCallback(object) - throw e - } - } - - #objectHandler(object, root) { - return { - get: (target, prop, receiver)=> - this.#getObjectProperty(object, target, prop, receiver), - set: (target, prop, val, receiver)=> - this.#setObjectProperty(object, root, target, prop, val, receiver), - deleteProperty: (target, prop)=> - this.#deleteObjectProperty(object, root, target, prop) - } - } - - #getObjectProperty(object, target, prop, receiver) { - if (typeof target[prop] === 'object' && target[prop] !== null) { - return new Proxy(Reflect.get(target, prop, receiver), this.#objectHandler(object, false)) - } else { - return Reflect.get(target, prop, receiver) - } - } - - #setObjectProperty(object, root, target, prop, val, receiver) { - // Store the original, perform the update, - // sync with server and restore original if error - const originalObject = Object.assign({}, object) - if (Reflect.set(target, prop, val, receiver)) { - this.#removeCallback(originalObject) - this.#updateCallback(object) - this.#request({ update: object }).catch(e=> { - this.#removeCallback(object) - this.#updateCallback(originalObject) - throw e - }) - return true - } else { return false } - } - - #deleteObjectProperty(object, root, target, prop) { - const originalObject = Object.assign({}, object) - if (root && ['_key', '_by', '_tags'].includes(prop)) { - // This is a deletion of the whole object - this.#removeCallback(object) - this.#request({ remove: object._key }).catch(e=> { - this.#updateCallback(originalObject) - throw e - }) - return true - } else { - if (Reflect.deleteProperty(target, prop)) { - this.#request({ update: object }).catch(e=> { - this.#updateCallback(originalObject) - throw e - }) - return true - } else { return false } - } - } - - async myTags() { - return await this.#request({ ls: null }) - } - - async objectByKey(userID, objectKey) { - return await this.#request({ get: { - _by: userID, - _key: objectKey - }}) - } - - objects(...tags) { - tags = tags.filter(tag=> tag!=null) - for (const tag of tags) { - if (!(tag in this.tagMap)) { - throw `You are not subscribed to '${tag}'` - } - } - - // Merge by UUIDs from all tags and - // convert to relevant objects - const uuids = new Set(tags.map(tag=>[...this.tagMap[tag].uuids]).flat()) - const objects = [...uuids].map(uuid=> this.objectMap[uuid]) - - // Return an array wrapped with graffiti functions - return new this.GraffitiArray(...objects) - } - - async subscribe(...tags) { - tags = tags.filter(tag=> tag!=null) - // Look at what is already subscribed to - const subscribingTags = [] - for (const tag of tags) { - if (tag in this.tagMap) { - // Increase the count - this.tagMap[tag].count++ - } else { - // Create a new slot - this.tagMap[tag] = { - uuids: new Set(), - count: 1 - } - subscribingTags.push(tag) - } - } - - // Try subscribing in the background - // but don't raise an error since - // the subscriptions will happen once connected - if (subscribingTags.length) - try { - await this.#request({ subscribe: subscribingTags }) - } catch {} - } - - async unsubscribe(...tags) { - tags = tags.filter(tag=> tag!=null) - // Decrease the count of each tag, - // removing and marking if necessary - const unsubscribingTags = [] - for (const tag of tags) { - this.tagMap[tag].count-- - - if (!this.tagMap[tag].count) { - unsubscribingTags.push(tag) - delete this.tagMap[tag] - } - } - - // Unsubscribe from all remaining tags - if (unsubscribingTags.length) - try { - await this.#request({ unsubscribe: unsubscribingTags }) - } catch {} - } - - async #onOpen() { - console.log("connected to the graffiti socket") - this.open = true - this.eventTarget.dispatchEvent(new Event("open")) - - // Clear data - for (let tag in this.tagMap) { - this.tagMap[tag].uuids = new Set() - } - for (let uuid in this.objectMap) delete this.objectMap[uuid] - - // Resubscribe - const tags = Object.keys(this.tagMap) - if (tags.length) await this.#request({ subscribe: tags }) - } - - // Utility function to get a universally unique string - // that represents a particular object - #objectUUID(object) { - if (!object._by || !object._key) { - throw { - type: 'error', - content: 'the object you are trying to identify does not have an owner or key', - object - } - } - return object._by + object._key - } -} diff --git a/index.html b/index.html new file mode 100644 index 0000000..d1b5991 --- /dev/null +++ b/index.html @@ -0,0 +1,73 @@ + + + + + Graffiti JS Demo + + + + + + + + + +

Graffiti Demo

+ +

Connection Status

+ +

+ Connected to the Graffiti server? {{ $graffitiConnected }} +

+ +

Logging In

+ +

+ +

+ +

+ My Graffiti ID is {{ $graffitiMyID }} +

+ +

Profile

+ +

+ My name is: +

+ + + +

Chatting

+ + + +

Moderation

+ + + +

Private Messaging

+ + + + diff --git a/plugins/vue/plugin.js b/plugins/vue/plugin.js deleted file mode 100644 index f04098e..0000000 --- a/plugins/vue/plugin.js +++ /dev/null @@ -1,76 +0,0 @@ -import { ref, reactive } from 'vue' -import Graffiti from '../../graffiti.js' - -export default { - install(app, options) { - - const graffitiURL = options && 'url' in options? - options.url : 'https://graffiti.garden' - - // Initialize graffiti with reactive entries - const graffiti = new Graffiti(graffitiURL, ()=>reactive({})) - - // Create a reactive variable that - // tracks connection state - const connectionState = ref(false) - ;(function waitForState(state) { - graffiti.connectionState(state).then(()=> { - connectionState.value = state - waitForState(!state) - })})(true) - Object.defineProperty(app.config.globalProperties, "$graffitiConnected", { - get: ()=> connectionState.value - }) - - // Latch on to the graffiti ID - // when the connection state first becomes true - let myID = null - Object.defineProperty(app.config.globalProperties, "$graffitiMyID", { - get: ()=> { - if (connectionState.value) myID = graffiti.myID - return myID - } - }) - - // Add static functions - for (const key of ['toggleLogIn', 'update', 'myTags', 'objectByKey']) { - const vueKey = '$graffiti' + key.charAt(0).toUpperCase() + key.slice(1) - app.config.globalProperties[vueKey] = graffiti[key].bind(graffiti) - } - - // A component for subscribing and - // unsubscribing to tags that returns - // a reactive array of the results - app.component('GraffitiObjects', { - - props: ['tags'], - - watch: { - tags: { - async handler(newTags, oldTags=[]) { - // Subscribe to the new tags - await graffiti.subscribe(...newTags) - // Unsubscribe to the existing tags - await graffiti.unsubscribe(...oldTags) - }, - immediate: true, - deep: true - } - }, - - // Handle unmounting too - unmount() { - graffiti.unsubscribe(this.tags) - }, - - computed: { - objects() { - return graffiti.objects(...this.tags) - } - }, - - template: '' - }) - - } -} diff --git a/src/array.js b/src/array.js deleted file mode 100644 index ae30012..0000000 --- a/src/array.js +++ /dev/null @@ -1,61 +0,0 @@ -// Extend the array class to expose update -// functionality, plus provide some -// useful helper methods -export default function(graffiti) { - - return class GraffitiArray extends Array { - - get mine() { - return this.filter(o=> o._by==graffiti.myID) - } - - get notMine() { - return this.filter(o=> o._by!=graffiti.myID) - } - - get authors() { - return [...new Set(this.map(o=> o._by))] - } - - removeMine() { - this.mine.map(o=> delete o._key) - } - - #getProperty(obj, propertyPath) { - // Split it up by periods - propertyPath = propertyPath.match(/([^\.]+)/g) - // Traverse down the path tree - for (const property of propertyPath) { - obj = obj[property] - } - return obj - } - - sortBy(propertyPath) { - - const sortOrder = propertyPath[0] == '-'? -1 : 1 - if (sortOrder < 0) propertyPath = propertyPath.substring(1) - - return this.sort((a, b)=> { - const propertyA = this.#getProperty(a, propertyPath) - const propertyB = this.#getProperty(b, propertyPath) - return sortOrder * ( - propertyA < propertyB? -1 : - propertyA > propertyB? 1 : 0 ) - }) - } - - groupBy(propertyPath) { - return this.reduce((chain, obj)=> { - const property = this.#getProperty(obj, propertyPath) - if (property in chain) { - chain[property].push(obj) - } else { - chain[property] = new GraffitiArray(obj) - } - return chain - }, {}) - } - - } -} diff --git a/src/auth.js b/src/auth.js deleted file mode 100644 index 8ee803d..0000000 --- a/src/auth.js +++ /dev/null @@ -1,125 +0,0 @@ -export default { - - async logIn(graffitiURL) { - // Generate a random client secret and state - const clientSecret = crypto.randomUUID() - const state = crypto.randomUUID() - - // The client ID is the secret's hex hash - const clientID = await this.sha256(clientSecret) - - // Store the client secret as a local variable - window.localStorage.setItem('graffitiClientSecret', clientSecret) - window.localStorage.setItem('graffitiClientID', clientID) - window.localStorage.setItem('graffitiAuthState', state) - - // Redirect to the login window - const loginURL = this.authURL(graffitiURL) - loginURL.searchParams.set('client_id', clientID) - loginURL.searchParams.set('redirect_uri', window.location.href) - loginURL.searchParams.set('state', state) - window.location.href = loginURL - }, - - async connect(graffitiURL) { - - // Check to see if we are already logged in - let token = window.localStorage.getItem('graffitiToken') - let myID = window.localStorage.getItem('graffitiID') - - if (!token || !myID) { - // Remove them both in case one exists - // and the other does not - token = myID = null - - // Check to see if we are redirecting back - const url = new URL(window.location) - if (url.searchParams.has('code')) { - - // Extract the code and state from the URL and strip it from the history - const code = url.searchParams.get('code') - const state = url.searchParams.get('state') - url.searchParams.delete('code') - url.searchParams.delete('state') - window.history.replaceState({}, '', url) - - // Get stored variables and remove them - const clientSecret = window.localStorage.getItem('graffitiClientSecret') - const clientID = window.localStorage.getItem('graffitiClientID') - const storedState = window.localStorage.getItem('graffitiAuthState') - window.localStorage.removeItem('graffitiClientSecret') - window.localStorage.removeItem('graffitiClientID') - window.localStorage.removeItem('graffitiAuthState') - - // Make sure state has been preserved - if (state != storedState) { - throw new Error("The state in local storage does not match the state sent by the server") - } - - // Construct the body of the POST - let form = new FormData() - form.append('client_id', clientID) - form.append('client_secret', clientSecret) - form.append('code', code) - - // Ask to exchange the code for a token - const tokenURL = this.authURL(graffitiURL) - tokenURL.pathname = '/token' - const response = await fetch(tokenURL, { - method: 'post', - body: form - }) - - // Make sure the response is OK - if (!response.ok) { - let reason = response.status + ": " - try { - reason += (await response.json()).detail - } catch (e) { - reason += response.statusText - } - - throw new Error(`The authorization code could not be exchanged for a token.\n\n${reason}`) - } - - // Parse out the token - const data = await response.json() - token = data.access_token - myID = data.owner_id - - // And make sure that the token is valid - if (!token || !myID) { - throw new Error(`The authorization token could not be parsed from the response.\n\n${data}`) - } - - // Store the token and ID - window.localStorage.setItem('graffitiToken', token) - window.localStorage.setItem('graffitiID', myID) - } - } - - return { myID, token } - - }, - - logOut() { - window.localStorage.removeItem('graffitiToken') - window.localStorage.removeItem('graffitiID') - window.location.reload() - }, - - authURL(graffitiURL) { - const url = new URL(graffitiURL) - url.host = "auth." + url.host - return url - }, - - async sha256(input) { - const encoder = new TextEncoder() - const inputBytes = encoder.encode(input) - const outputBuffer = await crypto.subtle.digest('SHA-256', inputBytes) - const outputArray = Array.from(new Uint8Array(outputBuffer)) - return outputArray.map(b => b.toString(16).padStart(2, '0')).join('') - } - -} diff --git a/src/logoot.js b/src/logoot.js deleted file mode 100644 index c8c7c02..0000000 --- a/src/logoot.js +++ /dev/null @@ -1,153 +0,0 @@ -export default { - - query(property) { - return { - [property]: { - $type: 'array', - $type: ['int', 'long'], - }, - $nor: [ - { [property]: { $gt: this.maxInt } }, - { [property]: { $lt: 0 } }, - ] - } - }, - - get before() { - return [] - }, - - get after() { - return [this.maxInt+1] - }, - - between(a, b, scale=100) { - // Strip zeros and find common length - const aLength = this.lengthWithoutZeros(a) - const bLength = this.lengthWithoutZeros(b) - const minLength = Math.min(aLength, bLength) - - // Initialize output - const out = [] - - // Find the break point where a[i] != b[i] - let i = 0 - while (i < minLength && a[i] == b[i]) { - out.push(a[i]) - i++ - } - - // Initialize upper and lower bounds for - // sampling the last digit - let lowerBound = 1 - let upperBound = this.maxInt - - if (i < minLength) { - // If the break happened before we hit - // the end of one of the arrays - - if (Math.abs(a[i] - b[i]) > 1) { - // If a[i] and b[i] are more than one - // away from each other, just sample - // between them - lowerBound = Math.min(a[i], b[i]) + 1 - upperBound = Math.max(a[i], b[i]) - 1 - } else { - // If they are one away no integers - // will fit in between, so add new layer - const lesser = (a[i] < b[i])? a : b - out.push(lesser[i]) - i++ - - while (i < lesser.length && lesser[i] >= this.maxInt) { - // If the lesser is at it's limit, - // we will need to add even more layers - out.push(lesser[i]) - i++ - } - - if (i < lesser.length) { - // Sample something greater than - // the lesser digit - lowerBound = lesser[i] + 1 - } - } - } else { - // The break happened because we hit - // the end of one of the arrays. - - if (aLength == bLength) { - // If they are entirely equal, - // there is nothing in between - // just return what we have - return out - } - - const longerLength = Math.max(aLength, bLength) - const longer = (a.length == longerLength)? a : b - while (i < longerLength && longer[i] == 0) { - // Skip past the zeros because we can't sample - // for digits less than zero - out.push(0) - i++ - } - - if (i < longerLength) { - if (longer[i] == 1) { - // If longer is at it's limit, - // we still need to add another layer - out.push(0) - } else { - upperBound = longer[i] - 1 - } - } - } - - // Create a random number in [0,1] but bias it to be small, - // so that numbers tend to increase by a small amount. - let random = Math.random() - random = -Math.log(1-random)/scale - random = Math.min(random, 1) - - // Finally, sample between the upper and lower bounds - out.push(Math.floor(random * (upperBound + 1 - lowerBound)) + lowerBound) - return out - }, - - compare(a, b) { - // Strip zeros and find common length - const aLength = this.lengthWithoutZeros(a) - const bLength = this.lengthWithoutZeros(b) - const minLength = Math.min(aLength, bLength) - - // See if there are any differences - for (let i = 0; i < minLength; i++) { - if (a[i] > b[i]) { - return 1 - } else if (a[i] < b[i]) { - return -1 - } - } - - // If they are all the same up til now, - // the longer one is bigger - if (aLength > bLength) { - return 1 - } else if (aLength < bLength) { - return -1 - } else { - return 0 - } - }, - - - lengthWithoutZeros(a) { - let length = a.length - while (length > 0 && a[length - 1] == 0) { - length-- - } - return length - }, - - maxInt: 9007199254740991, -} diff --git a/style.css b/style.css new file mode 100644 index 0000000..7bdf592 --- /dev/null +++ b/style.css @@ -0,0 +1,3 @@ +h2 { + margin-top: 1.5em; +} -- cgit v1.2.3-70-g09d2