diff --git a/README.md b/README.md index 2f2006b..07ca920 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Carbon is currently _technically_ usable as a chat app, but is very early in development. These important features still need to be implemented: +- Login GUI - Unreads - Chat history -- Typing indicators - Formatting - Emojis - Reactions diff --git a/package-lock.json b/package-lock.json index 94cc52b..e5806d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "carbon", + "name": "cosc212-assignment-1", "version": "1.0.0", "lockfileVersion": 1, "requires": true, diff --git a/spec.js b/spec.js index 450f7cc..38bb96e 100644 --- a/spec.js +++ b/spec.js @@ -2,136 +2,121 @@ module.exports = [ { type: "file", source: "/assets/fonts/whitney-500.woff", - target: "/static/whitney-500.woff", + target: "/static/whitney-500.woff" }, { type: "file", source: "/assets/fonts/whitney-400.woff", - target: "/static/whitney-400.woff", - }, - { - type: "js", - source: "/js/main.js", - target: "/static/main.js", + target: "/static/whitney-400.woff" }, { type: "js", source: "/js/basic.js", - target: "/static/basic.js", + target: "/static/basic.js" }, { type: "js", source: "/js/groups.js", - target: "/static/groups.js", + target: "/static/groups.js" }, { type: "js", source: "/js/chat-input.js", - target: "/static/chat-input.js", + target: "/static/chat-input.js" }, { type: "js", source: "/js/room-picker.js", - target: "/static/room-picker.js", + target: "/static/room-picker.js" }, { type: "js", source: "/js/store/store.js", - target: "/static/store/store.js", + target: "/static/store/store.js" }, { type: "js", source: "/js/store/Subscribable.js", - target: "/static/store/Subscribable.js", + target: "/static/store/Subscribable.js" }, { type: "js", source: "/js/store/SubscribeValue.js", - target: "/static/store/SubscribeValue.js", + target: "/static/store/SubscribeValue.js" }, { type: "js", source: "/js/store/SubscribeMapList.js", - target: "/static/store/SubscribeMapList.js", + target: "/static/store/SubscribeMapList.js" }, { type: "js", source: "/js/store/SubscribeSet.js", - target: "/static/store/SubscribeSet.js", + target: "/static/store/SubscribeSet.js" }, { type: "js", source: "/js/sync/sync.js", - target: "/static/sync/sync.js", + target: "/static/sync/sync.js" }, { type: "js", source: "/js/lsm.js", - target: "/static/lsm.js", + target: "/static/lsm.js" }, { type: "js", source: "/js/Timeline.js", - target: "/static/Timeline.js", + target: "/static/Timeline.js" }, { type: "js", source: "/js/Anchor.js", - target: "/static/Anchor.js", + target: "/static/Anchor.js" }, { type: "js", source: "/js/chat.js", - target: "/static/chat.js", + target: "/static/chat.js" }, { type: "js", source: "/js/functions.js", - target: "/static/functions.js", - }, - { - type: "js", - source: "/js/login.js", - target: "/static/login.js", + target: "/static/functions.js" }, { type: "file", source: "/assets/fonts/whitney-500.woff", - target: "/static/whitney-500.woff", + target: "/static/whitney-500.woff" }, { type: "file", source: "/assets/icons/directs.svg", - target: "/static/directs.svg", + target: "/static/directs.svg" }, { type: "file", source: "/assets/icons/channels.svg", - target: "/static/channels.svg", + target: "/static/channels.svg" }, { type: "file", source: "/assets/icons/join-event.svg", - target: "/static/join-event.svg", + target: "/static/join-event.svg" }, { type: "sass", source: "/sass/main.sass", - target: "/static/main.css", - }, - { - type: "sass", - source: "/sass/login.sass", - target: "/static/login.css", + target: "/static/main.css" }, { type: "pug", source: "/home.pug", - target: "/index.html", + target: "/index.html" }, { type: "pug", source: "/login.pug", - target: "/login/index.html", - }, -]; + target: "/login.html" + } +] diff --git a/src/home.pug b/src/home.pug index 09a00c7..68bbd21 100644 --- a/src/home.pug +++ b/src/home.pug @@ -33,14 +33,18 @@ doctype html html head meta(charset="utf-8") - title Carbon // var static = !{JSON.stringify([...static.entries()].reduce((a, c) => (a[c[0]] = getRelative(c[1]), a), {}))} script | var staticFiles = new Map( != JSON.stringify([...static.keys()].map(k => [k, getStatic(k)])) | ) link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass")) - script(type="module" src=getStatic("/js/main.js")) + script(type="module" src=getStatic("/js/groups.js")) + script(type="module" src=getStatic("/js/chat-input.js")) + script(type="module" src=getStatic("/js/room-picker.js")) + script(type="module" src=getStatic("/js/sync/sync.js")) + script(type="module" src=getStatic("/js/chat.js")) + title Carbon body main.main .c-groups @@ -52,4 +56,4 @@ html .c-chat__messages#c-chat-messages .c-chat__inner#c-chat .c-chat-input - textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea + textarea(placeholder="Send a message..." autocomplete="off").c-chat-input__textarea#c-chat-textarea \ No newline at end of file diff --git a/src/js/Timeline.js b/src/js/Timeline.js index 3e2a0b6..94e77bf 100644 --- a/src/js/Timeline.js +++ b/src/js/Timeline.js @@ -81,8 +81,6 @@ class Event extends ElemJS { } else { this.child(ejs("i").text("left the room")) } - } else if (this.data.type === "m.room.encrypted") { - this.child(ejs("i").text("Carbon does not yet support encrypted messages.")) } else { this.child(ejs("i").text(`Unsupported event type ${this.data.type}`)) } diff --git a/src/js/login.js b/src/js/login.js deleted file mode 100644 index de32932..0000000 --- a/src/js/login.js +++ /dev/null @@ -1,148 +0,0 @@ -import {q, ElemJS, ejs} from $to_relative "/js/basic.js" - -const password = q("#password") -const homeserver = q("#homeserver") - -class Username extends ElemJS { - constructor() { - super(q("#username")) - - this.on("change", this.updateServer.bind(this)) - } - - isValid() { - return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/) - } - - getUsername() { - return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1] - } - - getServer() { - const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/) - if (server && server[1]) return server[1] - else return null - } - - updateServer() { - if (!this.isValid()) return - if (this.getServer()) homeserver.value = this.getServer() - } -} - -const username = new Username() - -class Feedback extends ElemJS { - constructor() { - super(q("#feedback")) - this.loading = false - this.loadingIcon = ejs("span").class("loading-icon") - this.messageSpan = ejs("span") - this.child(this.messageSpan) - } - - setLoading(state) { - if (this.loading && !state) { - this.loadingIcon.remove() - } else if (!this.loading && state) { - this.childAt(0, this.loadingIcon) - } - this.loading = state - } - - message(content) { - if (content) { - this.class("form-feedback") - } else { - this.removeClass("form-feedback") - } - this.messageSpan.text(content) - } -} - -const feedback = new Feedback() - -class Form extends ElemJS { - constructor() { - super(q("#form")) - - this.processing = false - - this.on("submit", this.submit.bind(this)) - } - - async submit() { - if (this.processing) return - this.processing = true - if (!username.isValid()) return this.cancel("Username is not valid.") - - // Resolve homeserver address - let currentAddress = homeserver.value - let ok = false - while (!ok) { - if (!currentAddress.match(/^https?:\/\//)) currentAddress = "https://" + currentAddress - currentAddress = currentAddress.replace(/\/*$/, "") - this.status(`Looking up homeserver... trying ${currentAddress}`) - try { - // check if we found the actual matrix server - try { - const versions = await fetch(`${currentAddress}/_matrix/client/versions`).then(res => res.json()) - if (Array.isArray(versions.versions)) { - ok = true - break - } - } catch (e) {} - // find the next matrix server in the chain - const root = await fetch(`${currentAddress}/.well-known/matrix/client`).then(res => res.json()) - let nextAddress = root["m.homeserver"].base_url - nextAddress = nextAddress.replace(/\/*$/, "") - if (currentAddress === nextAddress) { - ok = true - } - currentAddress = nextAddress - } catch (e) { - return this.cancel(`Failed to look up server ${currentAddress}`) - } - } - - // Request access token - this.status("Logging in...") - const root = await fetch(`${currentAddress}/_matrix/client/r0/login`, { - method: "POST", - body: JSON.stringify({ - type: "m.login.password", - user: username.getUsername(), - password: password.value - }) - }).then(res => res.json()) - - if (!root.access_token) { - if (root.error) { - this.cancel(`Server said: ${root.error}`) - } else { - this.cancel("Login mysteriously failed.") - console.error(root) - } - return - } - - localStorage.setItem("mx_user_id", root.user_id) - localStorage.setItem("domain", currentAddress) - localStorage.setItem("access_token", root.access_token) - - location.assign("./") - } - - status(message) { - feedback.setLoading(true) - feedback.message(message) - } - - cancel(message) { - this.processing = false - feedback.setLoading(false) - feedback.message(message) - } -} - -const form = new Form() diff --git a/src/js/main.js b/src/js/main.js deleted file mode 100644 index 98dd38b..0000000 --- a/src/js/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import $to_relative "/js/groups.js" -import $to_relative "/js/chat-input.js" -import $to_relative "/js/room-picker.js" -import $to_relative "/js/sync/sync.js" -import $to_relative "/js/chat.js" - -if (!localStorage.getItem("access_token")) { - location.assign("./login") -} diff --git a/src/js/room-picker.js b/src/js/room-picker.js index 9fbbece..8e696de 100644 --- a/src/js/room-picker.js +++ b/src/js/room-picker.js @@ -108,12 +108,10 @@ class Room extends ElemJS { getIcon() { const avatar = this.data.state.events.find(e => e.type === "m.room.avatar") if (avatar) { - const url = avatar.content.url || avatar.content.avatar_url - if (url) { - return resolveMxc(url, 32, "crop") - } + return resolveMxc(avatar.content.url || avatar.content.avatar_url, 32, "crop") + } else { + return null } - return null } isDirect() { diff --git a/src/js/sync/sync.js b/src/js/sync/sync.js index f338e4d..bbd6b11 100644 --- a/src/js/sync/sync.js +++ b/src/js/sync/sync.js @@ -121,6 +121,4 @@ function syncLoop() { store.activeGroup.set(store.groups.get("directs").value()) -if (lsm.get("access_token")) { - syncLoop() -} +syncLoop() diff --git a/src/login.pug b/src/login.pug index 470ee82..105b6bd 100644 --- a/src/login.pug +++ b/src/login.pug @@ -1,32 +1,21 @@ doctype html html - head - meta(charset="utf-8") - title Carbon - meta(name="viewport" content="width=device-width, initial-scale=1") - link(rel="stylesheet" type="text/css" href=getStatic("/sass/login.sass")) - script(type="module" src=getStatic("/js/login.js")) - - body - main.main - .center-login-container - h1 Welcome to Carbon! - form.login-form(method="post" onsubmit="return false")#form - .data-input - .form-input-container - label(for="username") Username - input(type="text" name="username" autocomplete="username" placeholder="@username:server.tld" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username - - .form-input-container - label(for="password") Password - input(name="password" autocomplete="current-password" type="password" required)#password - - .form-input-container - label(for="homeserver") Homeserver - input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver - - #feedback - - .form-input-container - input(type="submit" value="Log in")#submit + head + meta(charset="utf-8") + link(rel="stylesheet" type="text/css" href=getStatic("/sass/main.sass")) + title Carbon + body + main.main + form + div + label(for="login") Username + input(type="text" name="login" autocomplete="username" placeholder="example:matrix.org" required)#login + div + label(for="password") Password + input(type="text" name="password" autocomplete="current-password" required)#password + div + label(for="homeserver") Homeserver + input(type="text" name="homeserver" value="matrix.org" required)#homeserver + div + input(type="submit" value="Login") diff --git a/src/sass/login.sass b/src/sass/login.sass deleted file mode 100644 index e4820ee..0000000 --- a/src/sass/login.sass +++ /dev/null @@ -1,81 +0,0 @@ -@use "./base" -@use "./colors.sass" as c - -.main - justify-content: center - align-items: center - -.center-login-container - display: flex - flex-flow: column - justify-content: center - align-items: center - width: min(100vw, 450px) - padding: max(1rem,3vw) 2rem - margin: 8px - box-shadow: 0px 2px 10px c.$darkest - background-color: c.$darker - border-radius: 5px - -.login-form - align-items: center - flex: 1 1 auto - width: 100% - display: flex - justify-content: space-around - flex-flow: column - -.data-input - width: 100% - -.form-input-container - width: 100% - display: flex - flex-direction: column - margin: 1em 0 - -.form-feedback - width: 100% - margin: -0.5em 0 -0.8em - -@keyframes spin - 0% - transform: rotate(0deg) - 100% - transform: rotate(180deg) - -.loading-icon - display: inline-block - background-color: #ccc - width: 12px - height: 12px - margin-right: 6px - animation: spin 0.7s infinite - -input, button - font-family: inherit - font-size: 17px - background-color: c.$mild - color: #eee - width: 100% - border-radius: 5px - box-sizing: border-box - transition: background-color 0.15s ease-out, border-color 0.15s ease-out - padding: 4px 9px - border: 0px - -input[type="text"],input[type="password"] - border: 3px solid transparent - margin: 0.4em 0px - - &:hover, &:focus - border-color: c.$milder - -button, input[type="submit"] - padding: 7px - - &:hover - background-color: c.$milder - -label - font-size: 18px