initial commit
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import {createEnum} from "./utils/enum.js";
|
||||
|
||||
const ROOMALIAS_PATTERN = /^#([^:]*):(.+)$/;
|
||||
const ROOMID_PATTERN = /^!([^:]*):(.+)$/;
|
||||
const EVENT_WITH_ROOMID_PATTERN = /^[!]([^:]*):(.+)\/\$([^:]+):(.+)$/;
|
||||
const EVENT_WITH_ROOMALIAS_PATTERN = /^[#]([^:]*):(.+)\/\$([^:]+):(.+)$/;
|
||||
const USERID_PATTERN = /^@([^:]+):(.+)$/;
|
||||
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
||||
|
||||
export const IdentifierKind = createEnum(
|
||||
"RoomId",
|
||||
"RoomAlias",
|
||||
"UserId",
|
||||
"GroupId",
|
||||
);
|
||||
|
||||
function asPrefix(identifierKind) {
|
||||
switch (identifierKind) {
|
||||
case IdentifierKind.RoomId: return "!";
|
||||
case IdentifierKind.RoomAlias: return "#";
|
||||
case IdentifierKind.GroupId: return "+";
|
||||
case IdentifierKind.UserId: return "@";
|
||||
default: throw new Error("invalid id kind " + identifierKind);
|
||||
}
|
||||
}
|
||||
|
||||
export const LinkKind = createEnum(
|
||||
"Room",
|
||||
"User",
|
||||
"Group",
|
||||
"Event"
|
||||
)
|
||||
|
||||
function orderedUnique(array) {
|
||||
const copy = [];
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
if (i === 0 || array.lastIndexOf(array[i], i - 1) === -1) {
|
||||
copy.push(array[i]);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
export class Link {
|
||||
static parseFragment(fragment) {
|
||||
let [identifier, queryParams] = fragment.split("?");
|
||||
|
||||
let viaServers = [];
|
||||
if (queryParams) {
|
||||
viaServers = queryParams.split("&")
|
||||
.map(pair => pair.split("="))
|
||||
.filter(([key, value]) => key === "via")
|
||||
.map(([,value]) => value);
|
||||
}
|
||||
|
||||
if (identifier.startsWith("#/")) {
|
||||
identifier = identifier.substr(2);
|
||||
}
|
||||
|
||||
let kind;
|
||||
let matches;
|
||||
// longest first, so they dont get caught by ROOMALIAS_PATTERN and ROOMID_PATTERN
|
||||
matches = EVENT_WITH_ROOMID_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const roomServer = matches[2];
|
||||
const messageServer = matches[4];
|
||||
const roomLocalPart = matches[1];
|
||||
const messageLocalPart = matches[3];
|
||||
return new Link(viaServers, IdentifierKind.RoomId, roomLocalPart, roomServer, messageLocalPart, messageServer);
|
||||
}
|
||||
matches = EVENT_WITH_ROOMALIAS_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const roomServer = matches[2];
|
||||
const messageServer = matches[4];
|
||||
const roomLocalPart = matches[1];
|
||||
const messageLocalPart = matches[3];
|
||||
return new Link(viaServers, IdentifierKind.RoomAlias, roomLocalPart, roomServer, messageLocalPart, messageServer);
|
||||
}
|
||||
matches = USERID_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const server = matches[2];
|
||||
const localPart = matches[1];
|
||||
return new Link(viaServers, IdentifierKind.UserId, localPart, server);
|
||||
}
|
||||
matches = ROOMALIAS_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const server = matches[2];
|
||||
const localPart = matches[1];
|
||||
return new Link(viaServers, IdentifierKind.RoomAlias, localPart, server);
|
||||
}
|
||||
matches = ROOMID_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const server = matches[2];
|
||||
const localPart = matches[1];
|
||||
return new Link(viaServers, IdentifierKind.RoomId, localPart, server);
|
||||
}
|
||||
matches = GROUPID_PATTERN.exec(identifier);
|
||||
if (matches) {
|
||||
const server = matches[2];
|
||||
const localPart = matches[1];
|
||||
return new Link(viaServers, IdentifierKind.GroupId, localPart, server);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
constructor(viaServers, identifierKind, localPart, server, messageLocalPart = null, messageServer = null) {
|
||||
const servers = [server];
|
||||
if (messageServer) {
|
||||
servers.push(messageServer);
|
||||
}
|
||||
servers.push(...viaServers);
|
||||
this.servers = orderedUnique(servers);
|
||||
this.identifierKind = identifierKind;
|
||||
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
|
||||
this.eventId = messageLocalPart ? `$${messageLocalPart}:${messageServer}` : null;
|
||||
}
|
||||
|
||||
get kind() {
|
||||
if (this.eventId) {
|
||||
return LinkKind.Event;
|
||||
}
|
||||
switch (this.identifierKind) {
|
||||
case IdentifierKind.RoomId:
|
||||
case IdentifierKind.RoomAlias:
|
||||
return LinkKind.Room;
|
||||
case IdentifierKind.UserId:
|
||||
return LinkKind.User;
|
||||
case IdentifierKind.GroupId:
|
||||
return LinkKind.Group;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js";
|
||||
|
||||
/**
|
||||
Bindable template. Renders once, and allows bindings for given nodes. If you need
|
||||
to change the structure on a condition, use a subtemplate (if)
|
||||
|
||||
supports
|
||||
- event handlers (attribute fn value with name that starts with on)
|
||||
- one way binding of attributes (other attribute fn value)
|
||||
- one way binding of text values (child fn value)
|
||||
- refs to get dom nodes
|
||||
- className binding returning object with className => enabled map
|
||||
- add subviews inside the template
|
||||
*/
|
||||
// TODO: should we rename this to BoundView or something? As opposed to StaticView ...
|
||||
export class TemplateView {
|
||||
constructor(value, render = undefined) {
|
||||
this._value = value;
|
||||
this._render = render;
|
||||
this._eventListeners = null;
|
||||
this._bindings = null;
|
||||
this._subViews = null;
|
||||
this._root = null;
|
||||
this._boundUpdateFromValue = null;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
_subscribe() {
|
||||
if (typeof this._value?.on === "function") {
|
||||
this._boundUpdateFromValue = this._updateFromValue.bind(this);
|
||||
this._value.on("change", this._boundUpdateFromValue);
|
||||
}
|
||||
}
|
||||
|
||||
_unsubscribe() {
|
||||
if (this._boundUpdateFromValue) {
|
||||
if (typeof this._value.off === "function") {
|
||||
this._value.off("change", this._boundUpdateFromValue);
|
||||
}
|
||||
this._boundUpdateFromValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
_attach() {
|
||||
if (this._eventListeners) {
|
||||
for (let {node, name, fn, useCapture} of this._eventListeners) {
|
||||
node.addEventListener(name, fn, useCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_detach() {
|
||||
if (this._eventListeners) {
|
||||
for (let {node, name, fn, useCapture} of this._eventListeners) {
|
||||
node.removeEventListener(name, fn, useCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mount(options) {
|
||||
const builder = new TemplateBuilder(this);
|
||||
if (this._render) {
|
||||
this._root = this._render(builder, this._value);
|
||||
} else if (this.render) { // overriden in subclass
|
||||
this._root = this.render(builder, this._value);
|
||||
} else {
|
||||
throw new Error("no render function passed in, or overriden in subclass");
|
||||
}
|
||||
const parentProvidesUpdates = options && options.parentProvidesUpdates;
|
||||
if (!parentProvidesUpdates) {
|
||||
this._subscribe();
|
||||
}
|
||||
this._attach();
|
||||
return this._root;
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this._detach();
|
||||
this._unsubscribe();
|
||||
if (this._subViews) {
|
||||
for (const v of this._subViews) {
|
||||
v.unmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root() {
|
||||
return this._root;
|
||||
}
|
||||
|
||||
update(value) {
|
||||
this._value = value;
|
||||
if (this._bindings) {
|
||||
for (const binding of this._bindings) {
|
||||
binding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateFromValue(changedProps) {
|
||||
this.update(this._value, changedProps);
|
||||
}
|
||||
|
||||
_addEventListener(node, name, fn, useCapture = false) {
|
||||
if (!this._eventListeners) {
|
||||
this._eventListeners = [];
|
||||
}
|
||||
this._eventListeners.push({node, name, fn, useCapture});
|
||||
}
|
||||
|
||||
_addBinding(bindingFn) {
|
||||
if (!this._bindings) {
|
||||
this._bindings = [];
|
||||
}
|
||||
this._bindings.push(bindingFn);
|
||||
}
|
||||
|
||||
_addSubView(view) {
|
||||
if (!this._subViews) {
|
||||
this._subViews = [];
|
||||
}
|
||||
this._subViews.push(view);
|
||||
}
|
||||
}
|
||||
|
||||
// what is passed to render
|
||||
class TemplateBuilder {
|
||||
constructor(templateView) {
|
||||
this._templateView = templateView;
|
||||
}
|
||||
|
||||
get _value() {
|
||||
return this._templateView._value;
|
||||
}
|
||||
|
||||
addEventListener(node, name, fn, useCapture = false) {
|
||||
this._templateView._addEventListener(node, name, fn, useCapture);
|
||||
}
|
||||
|
||||
_addAttributeBinding(node, name, fn) {
|
||||
let prevValue = undefined;
|
||||
const binding = () => {
|
||||
const newValue = fn(this._value);
|
||||
if (prevValue !== newValue) {
|
||||
prevValue = newValue;
|
||||
setAttribute(node, name, newValue);
|
||||
}
|
||||
};
|
||||
this._templateView._addBinding(binding);
|
||||
binding();
|
||||
}
|
||||
|
||||
_addClassNamesBinding(node, obj) {
|
||||
this._addAttributeBinding(node, "className", value => classNames(obj, value));
|
||||
}
|
||||
|
||||
_addTextBinding(fn) {
|
||||
const initialValue = fn(this._value);
|
||||
const node = text(initialValue);
|
||||
let prevValue = initialValue;
|
||||
const binding = () => {
|
||||
const newValue = fn(this._value);
|
||||
if (prevValue !== newValue) {
|
||||
prevValue = newValue;
|
||||
node.textContent = newValue+"";
|
||||
}
|
||||
};
|
||||
|
||||
this._templateView._addBinding(binding);
|
||||
return node;
|
||||
}
|
||||
|
||||
_setNodeAttributes(node, attributes) {
|
||||
for(let [key, value] of Object.entries(attributes)) {
|
||||
const isFn = typeof value === "function";
|
||||
// binding for className as object of className => enabled
|
||||
if (key === "className" && typeof value === "object" && value !== null) {
|
||||
if (objHasFns(value)) {
|
||||
this._addClassNamesBinding(node, value);
|
||||
} else {
|
||||
setAttribute(node, key, classNames(value));
|
||||
}
|
||||
} else if (key.startsWith("on") && key.length > 2 && isFn) {
|
||||
const eventName = key.substr(2, 1).toLowerCase() + key.substr(3);
|
||||
const handler = value;
|
||||
this._templateView._addEventListener(node, eventName, handler);
|
||||
} else if (isFn) {
|
||||
this._addAttributeBinding(node, key, value);
|
||||
} else {
|
||||
setAttribute(node, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setNodeChildren(node, children) {
|
||||
if (!Array.isArray(children)) {
|
||||
children = [children];
|
||||
}
|
||||
for (let child of children) {
|
||||
if (typeof child === "function") {
|
||||
child = this._addTextBinding(child);
|
||||
} else if (!child.nodeType) {
|
||||
// not a DOM node, turn into text
|
||||
child = text(child);
|
||||
}
|
||||
node.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
_addReplaceNodeBinding(fn, renderNode) {
|
||||
let prevValue = fn(this._value);
|
||||
let node = renderNode(null);
|
||||
|
||||
const binding = () => {
|
||||
const newValue = fn(this._value);
|
||||
if (prevValue !== newValue) {
|
||||
prevValue = newValue;
|
||||
const newNode = renderNode(node);
|
||||
if (node.parentNode) {
|
||||
node.parentNode.replaceChild(newNode, node);
|
||||
}
|
||||
node = newNode;
|
||||
}
|
||||
};
|
||||
this._templateView._addBinding(binding);
|
||||
return node;
|
||||
}
|
||||
|
||||
el(name, attributes, children) {
|
||||
return this.elNS(HTML_NS, name, attributes, children);
|
||||
}
|
||||
|
||||
elNS(ns, name, attributes, children) {
|
||||
if (attributes && isChildren(attributes)) {
|
||||
children = attributes;
|
||||
attributes = null;
|
||||
}
|
||||
|
||||
const node = document.createElementNS(ns, name);
|
||||
|
||||
if (attributes) {
|
||||
this._setNodeAttributes(node, attributes);
|
||||
}
|
||||
if (children) {
|
||||
this._setNodeChildren(node, children);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template
|
||||
// you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree).
|
||||
view(view) {
|
||||
let root;
|
||||
try {
|
||||
root = view.mount();
|
||||
} catch (err) {
|
||||
return errorToDOM(err);
|
||||
}
|
||||
this._templateView._addSubView(view);
|
||||
return root;
|
||||
}
|
||||
|
||||
// sugar
|
||||
createTemplate(render) {
|
||||
return vm => new TemplateView(vm, render);
|
||||
}
|
||||
|
||||
// map a value to a view, every time the value changes
|
||||
mapView(mapFn, viewCreator) {
|
||||
return this._addReplaceNodeBinding(mapFn, (prevNode) => {
|
||||
if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) {
|
||||
const subViews = this._templateView._subViews;
|
||||
const viewIdx = subViews.findIndex(v => v.root() === prevNode);
|
||||
if (viewIdx !== -1) {
|
||||
const [view] = subViews.splice(viewIdx, 1);
|
||||
view.unmount();
|
||||
}
|
||||
}
|
||||
const view = viewCreator(mapFn(this._value));
|
||||
if (view) {
|
||||
return this.view(view);
|
||||
} else {
|
||||
return document.createComment("node binding placeholder");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// creates a conditional subtemplate
|
||||
if(fn, viewCreator) {
|
||||
return this.mapView(
|
||||
value => !!fn(value),
|
||||
enabled => enabled ? viewCreator(this._value) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function errorToDOM(error) {
|
||||
const stack = new Error().stack;
|
||||
const callee = stack.split("\n")[1];
|
||||
return tag.div([
|
||||
tag.h2("Something went wrong…"),
|
||||
tag.h3(error.message),
|
||||
tag.p(`This occurred while running ${callee}.`),
|
||||
tag.pre(error.stack),
|
||||
]);
|
||||
}
|
||||
|
||||
function objHasFns(obj) {
|
||||
for(const value of Object.values(obj)) {
|
||||
if (typeof value === "function") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [ns, tags] of Object.entries(TAG_NAMES)) {
|
||||
for (const tag of tags) {
|
||||
TemplateBuilder.prototype[tag] = function(attributes, children) {
|
||||
return this.elNS(ns, tag, attributes, children);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this._handlersByName = {};
|
||||
}
|
||||
|
||||
emit(name, ...values) {
|
||||
const handlers = this._handlersByName[name];
|
||||
if (handlers) {
|
||||
for(const h of handlers) {
|
||||
h(...values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
on(name, callback) {
|
||||
let handlers = this._handlersByName[name];
|
||||
if (!handlers) {
|
||||
this.onFirstSubscriptionAdded(name);
|
||||
this._handlersByName[name] = handlers = new Set();
|
||||
}
|
||||
handlers.add(callback);
|
||||
return () => {
|
||||
this.off(name, callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(name, callback) {
|
||||
const handlers = this._handlersByName[name];
|
||||
if (handlers) {
|
||||
handlers.delete(callback);
|
||||
if (handlers.length === 0) {
|
||||
delete this._handlersByName[name];
|
||||
this.onLastSubscriptionRemoved(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewModel extends EventEmitter {
|
||||
emitChange() {
|
||||
this.emit("change");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class CreateLinkViewModel extends ViewModel {
|
||||
createLink(identifier) {
|
||||
this._link = Link.fromIdentifier(identifier);
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
get link() {
|
||||
this._link.toURL();
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// DOM helper functions
|
||||
|
||||
export function isChildren(children) {
|
||||
// children should be an not-object (that's the attributes), or a domnode, or an array
|
||||
return typeof children !== "object" || !!children.nodeType || Array.isArray(children);
|
||||
}
|
||||
|
||||
export function classNames(obj, value) {
|
||||
return Object.entries(obj).reduce((cn, [name, enabled]) => {
|
||||
if (typeof enabled === "function") {
|
||||
enabled = enabled(value);
|
||||
}
|
||||
if (enabled) {
|
||||
return cn + (cn.length ? " " : "") + name;
|
||||
} else {
|
||||
return cn;
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function setAttribute(el, name, value) {
|
||||
if (name === "className") {
|
||||
name = "class";
|
||||
}
|
||||
if (value === false) {
|
||||
el.removeAttribute(name);
|
||||
} else {
|
||||
if (value === true) {
|
||||
value = name;
|
||||
}
|
||||
el.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function el(elementName, attributes, children) {
|
||||
return elNS(HTML_NS, elementName, attributes, children);
|
||||
}
|
||||
|
||||
export function elNS(ns, elementName, attributes, children) {
|
||||
if (attributes && isChildren(attributes)) {
|
||||
children = attributes;
|
||||
attributes = null;
|
||||
}
|
||||
|
||||
const e = document.createElementNS(ns, elementName);
|
||||
|
||||
if (attributes) {
|
||||
for (let [name, value] of Object.entries(attributes)) {
|
||||
if (name === "className" && typeof value === "object" && value !== null) {
|
||||
value = classNames(value);
|
||||
}
|
||||
setAttribute(e, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (children) {
|
||||
if (!Array.isArray(children)) {
|
||||
children = [children];
|
||||
}
|
||||
for (let c of children) {
|
||||
if (!c.nodeType) {
|
||||
c = text(c);
|
||||
}
|
||||
e.appendChild(c);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
export function text(str) {
|
||||
return document.createTextNode(str);
|
||||
}
|
||||
|
||||
export const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
export const TAG_NAMES = {
|
||||
[HTML_NS]: [
|
||||
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
||||
"pre", "button", "time", "input", "textarea", "label", "form", "progress", "output"],
|
||||
[SVG_NS]: ["svg", "circle"]
|
||||
};
|
||||
|
||||
export const tag = {};
|
||||
|
||||
|
||||
for (const [ns, tags] of Object.entries(TAG_NAMES)) {
|
||||
for (const tagName of tags) {
|
||||
tag[tagName] = function(attributes, children) {
|
||||
return elNS(ns, tagName, attributes, children);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import {xhrRequest} from "./utils/xhr.js";
|
||||
import {validateHomeServer} from "./matrix/HomeServer.js";
|
||||
import {Link, LinkKind} from "./Link.js";
|
||||
|
||||
export async function main() {
|
||||
const link = Link.parseFragment(location.hash);
|
||||
if (!link) {
|
||||
throw new Error("bad link");
|
||||
}
|
||||
const hs = await validateHomeServer(xhrRequest, link.servers[0]);
|
||||
if (link.kind === LinkKind.User) {
|
||||
const profile = await hs.getUserProfile(link.identifier);
|
||||
const imageURL = hs.mxcUrlThumbnail(profile.avatar_url, 64, 64, "crop");
|
||||
console.log(imageURL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export async function validateHomeServer(request, baseURL) {
|
||||
if (!baseURL.startsWith("http://") && !baseURL.startsWith("https://")) {
|
||||
baseURL = `https://${baseURL}`;
|
||||
}
|
||||
{
|
||||
const {status, body} = await request(`${baseURL}/.well-known/matrix/client`, {method: "GET"}).response();
|
||||
if (status === 200) {
|
||||
const proposedBaseURL = body?.['m.homeserver']?.base_url;
|
||||
if (typeof proposedBaseURL === "string") {
|
||||
baseURL = proposedBaseURL;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const {status} = await request(`${baseURL}/_matrix/client/versions`, {method: "GET"}).response();
|
||||
if (status !== 200) {
|
||||
throw new Error(`Invalid versions response from ${baseURL}`);
|
||||
}
|
||||
}
|
||||
return new HomeServer(request, baseURL);
|
||||
}
|
||||
|
||||
export class HomeServer {
|
||||
constructor(request, baseURL) {
|
||||
this._request = request;
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
async getUserProfile(userId) {
|
||||
const {body} = await this._request(`${this.baseURL}/_matrix/client/r0/profile/${userId}`, {method: "GET"}).response();
|
||||
return body;
|
||||
}
|
||||
|
||||
getGroupProfile(groupId) {
|
||||
//`/_matrix/client/r0/groups/${groupId}/profile`
|
||||
}
|
||||
|
||||
getPublicRooms() {
|
||||
|
||||
}
|
||||
|
||||
mxcUrlThumbnail(url, width, height, method) {
|
||||
const parts = parseMxcUrl(url);
|
||||
if (parts) {
|
||||
const [serverName, mediaId] = parts;
|
||||
const httpUrl = `${this.baseURL}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
|
||||
return httpUrl + `?width=${width}&height=${height}&method=${method}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseMxcUrl(url) {
|
||||
const prefix = "mxc://";
|
||||
if (url.startsWith(prefix)) {
|
||||
return url.substr(prefix.length).split("/", 2);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function createEnum(...values) {
|
||||
const obj = {};
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Invalid enum value name" + value?.toString());
|
||||
}
|
||||
obj[value] = value;
|
||||
}
|
||||
return Object.freeze(obj);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message, isTimeout) {
|
||||
super(message || "ConnectionError");
|
||||
this.isTimeout = isTimeout;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "ConnectionError";
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
get name() {
|
||||
return "AbortError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AbortError,
|
||||
ConnectionError
|
||||
} from "./error.js";
|
||||
|
||||
function addCacheBuster(urlStr, random = Math.random) {
|
||||
// XHR doesn't have a good way to disable cache,
|
||||
// so add a random query param
|
||||
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
|
||||
if (urlStr.includes("?")) {
|
||||
urlStr = urlStr + "&";
|
||||
} else {
|
||||
urlStr = urlStr + "?";
|
||||
}
|
||||
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
|
||||
}
|
||||
|
||||
class RequestResult {
|
||||
constructor(promise, xhr) {
|
||||
this._promise = promise;
|
||||
this._xhr = xhr;
|
||||
}
|
||||
|
||||
abort() {
|
||||
this._xhr.abort();
|
||||
}
|
||||
|
||||
response() {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
|
||||
function createXhr(url, {method, headers, timeout, uploadProgress}) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url);
|
||||
|
||||
if (headers) {
|
||||
for(const [name, value] of headers.entries()) {
|
||||
try {
|
||||
xhr.setRequestHeader(name, value);
|
||||
} catch (err) {
|
||||
console.info(`Could not set ${name} header: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (timeout) {
|
||||
xhr.timeout = timeout;
|
||||
}
|
||||
|
||||
if (uploadProgress) {
|
||||
xhr.upload.addEventListener("progress", evt => uploadProgress(evt.loaded));
|
||||
}
|
||||
|
||||
return xhr;
|
||||
}
|
||||
|
||||
function xhrAsPromise(xhr, method, url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.addEventListener("load", () => resolve(xhr));
|
||||
xhr.addEventListener("abort", () => reject(new AbortError()));
|
||||
xhr.addEventListener("error", () => reject(new ConnectionError(`Error ${method} ${url}`)));
|
||||
xhr.addEventListener("timeout", () => reject(new ConnectionError(`Timeout ${method} ${url}`, true)));
|
||||
});
|
||||
}
|
||||
|
||||
export function xhrRequest(url, options) {
|
||||
let {cache, body, method} = options;
|
||||
if (!cache) {
|
||||
url = addCacheBuster(url);
|
||||
}
|
||||
const xhr = createXhr(url, options);
|
||||
const promise = xhrAsPromise(xhr, method, url).then(xhr => {
|
||||
const {status} = xhr;
|
||||
const body = JSON.parse(xhr.responseText);
|
||||
return {status, body};
|
||||
});
|
||||
|
||||
xhr.send(body || null);
|
||||
|
||||
return new RequestResult(promise, xhr);
|
||||
}
|
||||
Reference in New Issue
Block a user