forked from mystiq/hydrogen-web
Merge branch 'master' into bwindels/calls
This commit is contained in:
commit
d734a61447
40 changed files with 890 additions and 204 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,3 +8,4 @@ target
|
||||||
lib
|
lib
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.tmp
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
||||||
"lint-ci": "eslint src/",
|
"lint-ci": "eslint src/",
|
||||||
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
|
"test": "impunity --entry-point src/platform/web/main.js src/platform/web/Platform.js --force-esm-dirs lib/ src/ --root-dir src/",
|
||||||
"test:postcss": "impunity --entry-point scripts/postcss/test.js ",
|
"test:postcss": "impunity --entry-point scripts/postcss/tests/css-compile-variables.test.js scripts/postcss/tests/css-url-to-variables.test.js",
|
||||||
"start": "vite --port 3000",
|
"start": "vite --port 3000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:sdk": "./scripts/sdk/build.sh"
|
"build:sdk": "./scripts/sdk/build.sh"
|
||||||
|
@ -43,6 +43,7 @@
|
||||||
"node-html-parser": "^4.0.0",
|
"node-html-parser": "^4.0.0",
|
||||||
"postcss-css-variables": "^0.18.0",
|
"postcss-css-variables": "^0.18.0",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
|
"postcss-value-parser": "^4.2.0",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"typescript": "^4.4",
|
"typescript": "^4.4",
|
||||||
|
@ -54,7 +55,6 @@
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"base64-arraybuffer": "^0.2.0",
|
"base64-arraybuffer": "^0.2.0",
|
||||||
"dompurify": "^2.3.0",
|
"dompurify": "^2.3.0",
|
||||||
"off-color": "^2.0.0",
|
"off-color": "^2.0.0"
|
||||||
"postcss-value-parser": "^4.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
174
scripts/build-plugins/rollup-plugin-build-themes.js
Normal file
174
scripts/build-plugins/rollup-plugin-build-themes.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function readCSSSource(location) {
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
const path = require("path");
|
||||||
|
const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`);
|
||||||
|
const data = await fs.readFile(resolvedLocation);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendVariablesToCSS(variables, cssSource) {
|
||||||
|
return cssSource + `:root{\n${Object.entries(variables).reduce((acc, [key, value]) => acc + `--${key}: ${value};\n`, "")} }\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBundle(bundle) {
|
||||||
|
const chunkMap = new Map();
|
||||||
|
const assetMap = new Map();
|
||||||
|
let runtimeThemeChunk;
|
||||||
|
for (const [fileName, info] of Object.entries(bundle)) {
|
||||||
|
if (!fileName.endsWith(".css")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (info.type === "asset") {
|
||||||
|
/**
|
||||||
|
* So this is the css assetInfo that contains the asset hashed file name.
|
||||||
|
* We'll store it in a separate map indexed via fileName (unhashed) to avoid
|
||||||
|
* searching through the bundle array later.
|
||||||
|
*/
|
||||||
|
assetMap.set(info.name, info);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (info.facadeModuleId?.includes("type=runtime")) {
|
||||||
|
/**
|
||||||
|
* We have a separate field in manifest.source just for the runtime theme,
|
||||||
|
* so store this separately.
|
||||||
|
*/
|
||||||
|
runtimeThemeChunk = info;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const location = info.facadeModuleId?.match(/(.+)\/.+\.css/)?.[1];
|
||||||
|
if (!location) {
|
||||||
|
throw new Error("Cannot find location of css chunk!");
|
||||||
|
}
|
||||||
|
const array = chunkMap.get(location);
|
||||||
|
if (!array) {
|
||||||
|
chunkMap.set(location, [info]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
array.push(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { chunkMap, assetMap, runtimeThemeChunk };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function buildThemes(options) {
|
||||||
|
let manifest, variants, defaultDark, defaultLight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "build-themes",
|
||||||
|
enforce: "pre",
|
||||||
|
|
||||||
|
async buildStart() {
|
||||||
|
const { manifestLocations } = options;
|
||||||
|
for (const location of manifestLocations) {
|
||||||
|
manifest = require(`${location}/manifest.json`);
|
||||||
|
variants = manifest.values.variants;
|
||||||
|
const themeName = manifest.name;
|
||||||
|
for (const [variant, details] of Object.entries(variants)) {
|
||||||
|
const fileName = `theme-${themeName}-${variant}.css`;
|
||||||
|
if (details.default) {
|
||||||
|
// This is one of the default variants for this theme.
|
||||||
|
if (details.dark) {
|
||||||
|
defaultDark = fileName;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
defaultLight = fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// emit the css as built theme bundle
|
||||||
|
this.emitFile({
|
||||||
|
type: "chunk",
|
||||||
|
id: `${location}/theme.css?variant=${variant}`,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// emit the css as runtime theme bundle
|
||||||
|
this.emitFile({
|
||||||
|
type: "chunk",
|
||||||
|
id: `${location}/theme.css?type=runtime`,
|
||||||
|
fileName: `theme-${themeName}-runtime.css`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async load(id) {
|
||||||
|
const result = id.match(/(.+)\/theme.css\?variant=(.+)/);
|
||||||
|
if (result) {
|
||||||
|
const [, location, variant] = result;
|
||||||
|
const cssSource = await readCSSSource(location);
|
||||||
|
const config = variants[variant];
|
||||||
|
return await appendVariablesToCSS(config.variables, cssSource);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
transformIndexHtml(_, ctx) {
|
||||||
|
let darkThemeLocation, lightThemeLocation;
|
||||||
|
for (const [, bundle] of Object.entries(ctx.bundle)) {
|
||||||
|
if (bundle.name === defaultDark) {
|
||||||
|
darkThemeLocation = bundle.fileName;
|
||||||
|
}
|
||||||
|
if (bundle.name === defaultLight) {
|
||||||
|
lightThemeLocation = bundle.fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "link",
|
||||||
|
attrs: {
|
||||||
|
rel: "stylesheet",
|
||||||
|
type: "text/css",
|
||||||
|
media: "(prefers-color-scheme: dark)",
|
||||||
|
href: `./${darkThemeLocation}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "link",
|
||||||
|
attrs: {
|
||||||
|
rel: "stylesheet",
|
||||||
|
type: "text/css",
|
||||||
|
media: "(prefers-color-scheme: light)",
|
||||||
|
href: `./${lightThemeLocation}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
generateBundle(_, bundle) {
|
||||||
|
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
|
||||||
|
for (const [location, chunkArray] of chunkMap) {
|
||||||
|
const manifest = require(`${location}/manifest.json`);
|
||||||
|
const compiledVariables = options.compiledVariables.get(location);
|
||||||
|
const derivedVariables = compiledVariables["derived-variables"];
|
||||||
|
const icon = compiledVariables["icon"];
|
||||||
|
manifest.source = {
|
||||||
|
"built-asset": chunkArray.map(chunk => assetMap.get(chunk.fileName).fileName),
|
||||||
|
"runtime-asset": assetMap.get(runtimeThemeChunk.fileName).fileName,
|
||||||
|
"derived-variables": derivedVariables,
|
||||||
|
"icon": icon
|
||||||
|
};
|
||||||
|
const name = `theme-${manifest.name}.json`;
|
||||||
|
this.emitFile({
|
||||||
|
type: "asset",
|
||||||
|
name,
|
||||||
|
source: JSON.stringify(manifest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,17 +43,32 @@ function parseDeclarationValue(value) {
|
||||||
const parsed = valueParser(value);
|
const parsed = valueParser(value);
|
||||||
const variables = [];
|
const variables = [];
|
||||||
parsed.walk(node => {
|
parsed.walk(node => {
|
||||||
if (node.type !== "function" && node.value !== "var") {
|
if (node.type !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
switch (node.value) {
|
||||||
|
case "var": {
|
||||||
const variable = node.nodes[0];
|
const variable = node.nodes[0];
|
||||||
variables.push(variable.value);
|
variables.push(variable.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "url": {
|
||||||
|
const url = node.nodes[0].value;
|
||||||
|
// resolve url with some absolute url so that we get the query params without using regex
|
||||||
|
const params = new URL(url, "file://foo/bar/").searchParams;
|
||||||
|
const primary = params.get("primary");
|
||||||
|
const secondary = params.get("secondary");
|
||||||
|
if (primary) { variables.push(primary); }
|
||||||
|
if (secondary) { variables.push(secondary); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return variables;
|
return variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDerivedVariable(decl, derive) {
|
function resolveDerivedVariable(decl, derive) {
|
||||||
const RE_VARIABLE_VALUE = /--((.+)--(.+)-(.+))/;
|
const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/;
|
||||||
const variableCollection = parseDeclarationValue(decl.value);
|
const variableCollection = parseDeclarationValue(decl.value);
|
||||||
for (const variable of variableCollection) {
|
for (const variable of variableCollection) {
|
||||||
const matches = variable.match(RE_VARIABLE_VALUE);
|
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||||
|
@ -94,6 +109,15 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
|
||||||
root.append(newRule);
|
root.append(newRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateMapWithDerivedVariables(map, cssFileLocation) {
|
||||||
|
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
|
||||||
|
const derivedVariables = [
|
||||||
|
...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))),
|
||||||
|
...([...aliasMap.entries()].map(([alias, variable]) => `${alias}=${variable}`))
|
||||||
|
];
|
||||||
|
map.set(location, { "derived-variables": derivedVariables });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback derive
|
* @callback derive
|
||||||
* @param {string} value - The base value on which an operation is applied
|
* @param {string} value - The base value on which an operation is applied
|
||||||
|
@ -104,6 +128,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
|
||||||
*
|
*
|
||||||
* @param {Object} opts - Options for the plugin
|
* @param {Object} opts - Options for the plugin
|
||||||
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
|
* @param {derive} opts.derive - The callback which contains the logic for resolving derived variables
|
||||||
|
* @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced
|
||||||
*/
|
*/
|
||||||
module.exports = (opts = {}) => {
|
module.exports = (opts = {}) => {
|
||||||
aliasMap = new Map();
|
aliasMap = new Map();
|
||||||
|
@ -112,7 +137,12 @@ module.exports = (opts = {}) => {
|
||||||
return {
|
return {
|
||||||
postcssPlugin: "postcss-compile-variables",
|
postcssPlugin: "postcss-compile-variables",
|
||||||
|
|
||||||
Once(root, {Rule, Declaration}) {
|
Once(root, {Rule, Declaration, result}) {
|
||||||
|
const cssFileLocation = root.source.input.from;
|
||||||
|
if (cssFileLocation.includes("type=runtime")) {
|
||||||
|
// If this is a runtime theme, don't derive variables.
|
||||||
|
return;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
Go through the CSS file once to extract all aliases and base variables.
|
Go through the CSS file once to extract all aliases and base variables.
|
||||||
We use these when resolving derived variables later.
|
We use these when resolving derived variables later.
|
||||||
|
@ -120,6 +150,16 @@ module.exports = (opts = {}) => {
|
||||||
root.walkDecls(decl => extract(decl));
|
root.walkDecls(decl => extract(decl));
|
||||||
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive));
|
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive));
|
||||||
addResolvedVariablesToRootSelector(root, {Rule, Declaration});
|
addResolvedVariablesToRootSelector(root, {Rule, Declaration});
|
||||||
|
if (opts.compiledVariables){
|
||||||
|
populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation);
|
||||||
|
}
|
||||||
|
// Publish both the base-variables and derived-variables to the other postcss-plugins
|
||||||
|
const combinedMap = new Map([...baseVariables, ...resolvedMap]);
|
||||||
|
result.messages.push({
|
||||||
|
type: "resolved-variable-map",
|
||||||
|
plugin: "postcss-compile-variables",
|
||||||
|
colorMap: combinedMap,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
87
scripts/postcss/css-url-processor.js
Normal file
87
scripts/postcss/css-url-processor.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const valueParser = require("postcss-value-parser");
|
||||||
|
const resolve = require("path").resolve;
|
||||||
|
|
||||||
|
function colorsFromURL(url, colorMap) {
|
||||||
|
const params = new URL(`file://${url}`).searchParams;
|
||||||
|
const primary = params.get("primary");
|
||||||
|
if (!primary) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const secondary = params.get("secondary");
|
||||||
|
const primaryColor = colorMap.get(primary);
|
||||||
|
const secondaryColor = colorMap.get(secondary);
|
||||||
|
if (!primaryColor) {
|
||||||
|
throw new Error(`Variable ${primary} not found in resolved color variables!`);
|
||||||
|
}
|
||||||
|
if (secondary && !secondaryColor) {
|
||||||
|
throw new Error(`Variable ${secondary} not found in resolved color variables!`);
|
||||||
|
}
|
||||||
|
return [primaryColor, secondaryColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
function processURL(decl, replacer, colorMap) {
|
||||||
|
const value = decl.value;
|
||||||
|
const parsed = valueParser(value);
|
||||||
|
parsed.walk(async node => {
|
||||||
|
if (node.type !== "function" || node.value !== "url") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const urlStringNode = node.nodes[0];
|
||||||
|
const oldURL = urlStringNode.value;
|
||||||
|
const cssPath = decl.source?.input.file.replace(/[^/]*$/, "");
|
||||||
|
const oldURLAbsolute = resolve(cssPath, oldURL);
|
||||||
|
const colors = colorsFromURL(oldURLAbsolute, colorMap);
|
||||||
|
if (!colors) {
|
||||||
|
// If no primary color is provided via url params, then this url need not be handled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newURL = replacer(oldURLAbsolute.replace(/\?.+/, ""), ...colors);
|
||||||
|
if (!newURL) {
|
||||||
|
throw new Error("Replacer failed to produce a replacement URL!");
|
||||||
|
}
|
||||||
|
urlStringNode.value = newURL;
|
||||||
|
});
|
||||||
|
decl.assign({prop: decl.prop, value: parsed.toString()})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* @type {import('postcss').PluginCreator}
|
||||||
|
*/
|
||||||
|
module.exports = (opts = {}) => {
|
||||||
|
return {
|
||||||
|
postcssPlugin: "postcss-url-to-variable",
|
||||||
|
|
||||||
|
Once(root, {result}) {
|
||||||
|
/*
|
||||||
|
postcss-compile-variables should have sent the list of resolved colours down via results
|
||||||
|
*/
|
||||||
|
const {colorMap} = result.messages.find(m => m.type === "resolved-variable-map");
|
||||||
|
if (!colorMap) {
|
||||||
|
throw new Error("Postcss results do not contain resolved colors!");
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Go through each declaration and if it contains an URL, replace the url with the result
|
||||||
|
of running replacer(url)
|
||||||
|
*/
|
||||||
|
root.walkDecls(decl => processURL(decl, opts.replacer, colorMap));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.postcss = true;
|
85
scripts/postcss/css-url-to-variables.js
Normal file
85
scripts/postcss/css-url-to-variables.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const valueParser = require("postcss-value-parser");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This plugin extracts content inside url() into css variables and adds the variables to the root section.
|
||||||
|
* This plugin is used in conjunction with css-url-processor plugin to colorize svg icons.
|
||||||
|
*/
|
||||||
|
let counter;
|
||||||
|
let urlVariables;
|
||||||
|
const idToPrepend = "icon-url";
|
||||||
|
|
||||||
|
function findAndReplaceUrl(decl) {
|
||||||
|
const value = decl.value;
|
||||||
|
const parsed = valueParser(value);
|
||||||
|
parsed.walk(node => {
|
||||||
|
if (node.type !== "function" || node.value !== "url") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = node.nodes[0].value;
|
||||||
|
if (!url.match(/\.svg\?primary=.+/)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const variableName = `${idToPrepend}-${counter++}`;
|
||||||
|
urlVariables.set(variableName, url);
|
||||||
|
node.value = "var";
|
||||||
|
node.nodes = [{ type: "word", value: `--${variableName}` }];
|
||||||
|
});
|
||||||
|
decl.assign({prop: decl.prop, value: parsed.toString()})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) {
|
||||||
|
const newRule = new Rule({ selector: ":root", source: root.source });
|
||||||
|
// Add derived css variables to :root
|
||||||
|
urlVariables.forEach((value, key) => {
|
||||||
|
const declaration = new Declaration({ prop: `--${key}`, value: `url("${value}")`});
|
||||||
|
newRule.append(declaration);
|
||||||
|
});
|
||||||
|
root.append(newRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateMapWithIcons(map, cssFileLocation) {
|
||||||
|
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
|
||||||
|
const sharedObject = map.get(location);
|
||||||
|
sharedObject["icon"] = Object.fromEntries(urlVariables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* *
|
||||||
|
* @type {import('postcss').PluginCreator}
|
||||||
|
*/
|
||||||
|
module.exports = (opts = {}) => {
|
||||||
|
urlVariables = new Map();
|
||||||
|
counter = 0;
|
||||||
|
return {
|
||||||
|
postcssPlugin: "postcss-url-to-variable",
|
||||||
|
|
||||||
|
Once(root, { Rule, Declaration }) {
|
||||||
|
root.walkDecls(decl => findAndReplaceUrl(decl));
|
||||||
|
if (urlVariables.size) {
|
||||||
|
addResolvedVariablesToRootSelector(root, { Rule, Declaration });
|
||||||
|
}
|
||||||
|
if (opts.compiledVariables){
|
||||||
|
const cssFileLocation = root.source.input.from;
|
||||||
|
populateMapWithIcons(opts.compiledVariables, cssFileLocation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.postcss = true;
|
||||||
|
|
45
scripts/postcss/svg-colorizer.js
Normal file
45
scripts/postcss/svg-colorizer.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
/**
|
||||||
|
* Builds a new svg with the colors replaced and returns its location.
|
||||||
|
* @param {string} svgLocation The location of the input svg file
|
||||||
|
* @param {string} primaryColor Primary color for the new svg
|
||||||
|
* @param {string} secondaryColor Secondary color for the new svg
|
||||||
|
*/
|
||||||
|
module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) {
|
||||||
|
const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"});
|
||||||
|
let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor);
|
||||||
|
coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor);
|
||||||
|
if (svgCode === coloredSVGCode) {
|
||||||
|
throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive).");
|
||||||
|
}
|
||||||
|
const fileName = svgLocation.match(/.+\/(.+\.svg)/)[1];
|
||||||
|
const outputPath = path.resolve(__dirname, "../../.tmp");
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(outputPath);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e.code !== "EEXIST") {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputFile = `${outputPath}/${fileName}`;
|
||||||
|
fs.writeFileSync(outputFile, coloredSVGCode);
|
||||||
|
return outputFile;
|
||||||
|
}
|
30
scripts/postcss/tests/common.js
Normal file
30
scripts/postcss/tests/common.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const postcss = require("postcss");
|
||||||
|
|
||||||
|
module.exports.createTestRunner = function (plugin) {
|
||||||
|
return async function run(input, output, opts = {}, assert) {
|
||||||
|
let result = await postcss([plugin(opts)]).process(input, { from: undefined, });
|
||||||
|
assert.strictEqual(
|
||||||
|
result.css.replaceAll(/\s/g, ""),
|
||||||
|
output.replaceAll(/\s/g, "")
|
||||||
|
);
|
||||||
|
assert.strictEqual(result.warnings().length, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
const offColor = require("off-color").offColor;
|
const offColor = require("off-color").offColor;
|
||||||
const postcss = require("postcss");
|
const postcss = require("postcss");
|
||||||
const plugin = require("./css-compile-variables");
|
const plugin = require("../css-compile-variables");
|
||||||
const derive = require("./color").derive;
|
const derive = require("../color").derive;
|
||||||
|
const run = require("./common").createTestRunner(plugin);
|
||||||
async function run(input, output, opts = {}, assert) {
|
|
||||||
let result = await postcss([plugin({ ...opts, derive })]).process(input, { from: undefined, });
|
|
||||||
assert.strictEqual(
|
|
||||||
result.css.replaceAll(/\s/g, ""),
|
|
||||||
output.replaceAll(/\s/g, "")
|
|
||||||
);
|
|
||||||
assert.strictEqual(result.warnings().length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.tests = function tests() {
|
module.exports.tests = function tests() {
|
||||||
return {
|
return {
|
||||||
|
@ -46,7 +38,7 @@ module.exports.tests = function tests() {
|
||||||
--foo-color--lighter-50: ${transformedColor.hex()};
|
--foo-color--lighter-50: ${transformedColor.hex()};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await run( inputCSS, outputCSS, {}, assert);
|
await run( inputCSS, outputCSS, {derive}, assert);
|
||||||
},
|
},
|
||||||
|
|
||||||
"derived variables work with alias": async (assert) => {
|
"derived variables work with alias": async (assert) => {
|
||||||
|
@ -66,7 +58,7 @@ module.exports.tests = function tests() {
|
||||||
--my-alias--lighter-15: ${aliasLighter};
|
--my-alias--lighter-15: ${aliasLighter};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await run(inputCSS, outputCSS, { }, assert);
|
await run(inputCSS, outputCSS, {derive}, assert);
|
||||||
},
|
},
|
||||||
|
|
||||||
"derived variable throws if base not present in config": async (assert) => {
|
"derived variable throws if base not present in config": async (assert) => {
|
||||||
|
@ -94,8 +86,9 @@ module.exports.tests = function tests() {
|
||||||
--foo-color--darker-20: ${transformedColor2.hex()};
|
--foo-color--darker-20: ${transformedColor2.hex()};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await run( inputCSS, outputCSS, { }, assert);
|
await run( inputCSS, outputCSS, {derive}, assert);
|
||||||
},
|
},
|
||||||
|
|
||||||
"multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => {
|
"multiple aliased-derived variable in single declaration is parsed correctly": async (assert) => {
|
||||||
const inputCSS = `
|
const inputCSS = `
|
||||||
:root {
|
:root {
|
||||||
|
@ -115,7 +108,49 @@ module.exports.tests = function tests() {
|
||||||
--my-alias--darker-20: ${transformedColor2.hex()};
|
--my-alias--darker-20: ${transformedColor2.hex()};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
await run( inputCSS, outputCSS, { }, assert);
|
await run( inputCSS, outputCSS, {derive}, assert);
|
||||||
|
},
|
||||||
|
|
||||||
|
"compiledVariables map is populated": async (assert) => {
|
||||||
|
const compiledVariables = new Map();
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--icon-color: #fff;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
background: var(--icon-color--darker-20);
|
||||||
|
--my-alias: var(--icon-color--darker-20);
|
||||||
|
color: var(--my-alias--lighter-15);
|
||||||
|
}`;
|
||||||
|
await postcss([plugin({ derive, compiledVariables })]).process(inputCSS, { from: "/foo/bar/test.css", });
|
||||||
|
const actualArray = compiledVariables.get("/foo/bar")["derived-variables"];
|
||||||
|
const expectedArray = ["icon-color--darker-20", "my-alias=icon-color--darker-20", "my-alias--lighter-15"];
|
||||||
|
assert.deepStrictEqual(actualArray.sort(), expectedArray.sort());
|
||||||
|
},
|
||||||
|
|
||||||
|
"derived variable are supported in urls": async (assert) => {
|
||||||
|
const inputCSS = `
|
||||||
|
:root {
|
||||||
|
--foo-color: #ff0;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
background-color: var(--foo-color--lighter-50);
|
||||||
|
background: url("./foo/bar/icon.svg?primary=foo-color--darker-5");
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
background: url("foo/bar/icon.svg");
|
||||||
|
}`;
|
||||||
|
const transformedColorLighter = offColor("#ff0").lighten(0.5);
|
||||||
|
const transformedColorDarker = offColor("#ff0").darken(0.05);
|
||||||
|
const outputCSS =
|
||||||
|
inputCSS +
|
||||||
|
`
|
||||||
|
:root {
|
||||||
|
--foo-color--lighter-50: ${transformedColorLighter.hex()};
|
||||||
|
--foo-color--darker-5: ${transformedColorDarker.hex()};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run( inputCSS, outputCSS, {derive}, assert);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
71
scripts/postcss/tests/css-url-to-variables.test.js
Normal file
71
scripts/postcss/tests/css-url-to-variables.test.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const plugin = require("../css-url-to-variables");
|
||||||
|
const run = require("./common").createTestRunner(plugin);
|
||||||
|
const postcss = require("postcss");
|
||||||
|
|
||||||
|
module.exports.tests = function tests() {
|
||||||
|
return {
|
||||||
|
"url is replaced with variable": async (assert) => {
|
||||||
|
const inputCSS = `div {
|
||||||
|
background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20");
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
|
||||||
|
}`;
|
||||||
|
const outputCSS =
|
||||||
|
`div {
|
||||||
|
background: no-repeat center/80% var(--icon-url-0);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: var(--icon-url-1);
|
||||||
|
}`+
|
||||||
|
`
|
||||||
|
:root {
|
||||||
|
--icon-url-0: url("../img/image.svg?primary=main-color--darker-20");
|
||||||
|
--icon-url-1: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
await run(inputCSS, outputCSS, { }, assert);
|
||||||
|
},
|
||||||
|
"non svg urls without query params are not replaced": async (assert) => {
|
||||||
|
const inputCSS = `div {
|
||||||
|
background: no-repeat url("./img/foo/bar/image.png");
|
||||||
|
}`;
|
||||||
|
await run(inputCSS, inputCSS, {}, assert);
|
||||||
|
},
|
||||||
|
"map is populated with icons": async (assert) => {
|
||||||
|
const compiledVariables = new Map();
|
||||||
|
compiledVariables.set("/foo/bar", { "derived-variables": ["background-color--darker-20", "accent-color--lighter-15"] });
|
||||||
|
const inputCSS = `div {
|
||||||
|
background: no-repeat center/80% url("../img/image.svg?primary=main-color--darker-20");
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: url("/home/foo/bar/cool.svg?primary=blue&secondary=green");
|
||||||
|
}`;
|
||||||
|
const expectedObject = {
|
||||||
|
"icon-url-0": "../img/image.svg?primary=main-color--darker-20",
|
||||||
|
"icon-url-1": "/home/foo/bar/cool.svg?primary=blue&secondary=green",
|
||||||
|
};
|
||||||
|
await postcss([plugin({compiledVariables})]).process(inputCSS, { from: "/foo/bar/test.css", });
|
||||||
|
const sharedVariable = compiledVariables.get("/foo/bar");
|
||||||
|
assert.deepEqual(["background-color--darker-20", "accent-color--lighter-15"], sharedVariable["derived-variables"]);
|
||||||
|
assert.deepEqual(expectedObject, sharedVariable["icon"]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "hydrogen-view-sdk",
|
"name": "hydrogen-view-sdk",
|
||||||
"description": "Embeddable matrix client library, including view components",
|
"description": "Embeddable matrix client library, including view components",
|
||||||
"version": "0.0.9",
|
"version": "0.0.10",
|
||||||
"main": "./hydrogen.es.js",
|
"main": "./hydrogen.es.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,14 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
|
||||||
return this._options[name];
|
return this._options[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) {
|
||||||
|
const segmentObservable = this.navigation.observe(type);
|
||||||
|
const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => {
|
||||||
|
onChange(value, type);
|
||||||
|
})
|
||||||
|
this.track(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
track<D extends Disposable>(disposable: D): D {
|
track<D extends Disposable>(disposable: D): D {
|
||||||
if (!this.disposables) {
|
if (!this.disposables) {
|
||||||
this.disposables = new Disposables();
|
this.disposables = new Disposables();
|
||||||
|
|
|
@ -20,18 +20,21 @@ import {ComposerViewModel} from "./ComposerViewModel.js"
|
||||||
import {CallViewModel} from "./CallViewModel"
|
import {CallViewModel} from "./CallViewModel"
|
||||||
import {PickMapObservableValue} from "../../../observable/value/PickMapObservableValue";
|
import {PickMapObservableValue} from "../../../observable/value/PickMapObservableValue";
|
||||||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
|
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
|
||||||
import {tilesCreator} from "./timeline/tilesCreator.js";
|
|
||||||
import {ViewModel} from "../../ViewModel";
|
import {ViewModel} from "../../ViewModel";
|
||||||
import {imageToInfo} from "../common.js";
|
import {imageToInfo} from "../common.js";
|
||||||
import {LocalMedia} from "../../../matrix/calls/LocalMedia";
|
import {LocalMedia} from "../../../matrix/calls/LocalMedia";
|
||||||
|
// TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry
|
||||||
|
// this is a breaking SDK change though to make this option mandatory
|
||||||
|
import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index";
|
||||||
|
|
||||||
export class RoomViewModel extends ViewModel {
|
export class RoomViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {room} = options;
|
const {room, tileClassForEntry} = options;
|
||||||
this._room = room;
|
this._room = room;
|
||||||
this._timelineVM = null;
|
this._timelineVM = null;
|
||||||
this._tilesCreator = null;
|
this._tileClassForEntry = tileClassForEntry ?? defaultTileClassForEntry;
|
||||||
|
this._tileOptions = undefined;
|
||||||
this._onRoomChange = this._onRoomChange.bind(this);
|
this._onRoomChange = this._onRoomChange.bind(this);
|
||||||
this._timelineError = null;
|
this._timelineError = null;
|
||||||
this._sendError = null;
|
this._sendError = null;
|
||||||
|
@ -73,13 +76,14 @@ export class RoomViewModel extends ViewModel {
|
||||||
this._room.on("change", this._onRoomChange);
|
this._room.on("change", this._onRoomChange);
|
||||||
try {
|
try {
|
||||||
const timeline = await this._room.openTimeline();
|
const timeline = await this._room.openTimeline();
|
||||||
this._tilesCreator = tilesCreator(this.childOptions({
|
this._tileOptions = this.childOptions({
|
||||||
session: this.getOption("session"),
|
session: this.getOption("session"),
|
||||||
roomVM: this,
|
roomVM: this,
|
||||||
timeline,
|
timeline,
|
||||||
}));
|
tileClassForEntry: this._tileClassForEntry,
|
||||||
|
});
|
||||||
this._timelineVM = this.track(new TimelineViewModel(this.childOptions({
|
this._timelineVM = this.track(new TimelineViewModel(this.childOptions({
|
||||||
tilesCreator: this._tilesCreator,
|
tileOptions: this._tileOptions,
|
||||||
timeline,
|
timeline,
|
||||||
})));
|
})));
|
||||||
this.emitChange("timelineViewModel");
|
this.emitChange("timelineViewModel");
|
||||||
|
@ -189,7 +193,12 @@ export class RoomViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createTile(entry) {
|
_createTile(entry) {
|
||||||
return this._tilesCreator(entry);
|
if (this._tileOptions) {
|
||||||
|
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||||
|
if (Tile) {
|
||||||
|
return new Tile(entry, this._tileOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _sendMessage(message, replyingTo) {
|
async _sendMessage(message, replyingTo) {
|
||||||
|
|
|
@ -222,7 +222,7 @@ export function tests() {
|
||||||
};
|
};
|
||||||
const tiles = new MappedList(timeline.entries, entry => {
|
const tiles = new MappedList(timeline.entries, entry => {
|
||||||
if (entry.eventType === "m.room.message") {
|
if (entry.eventType === "m.room.message") {
|
||||||
return new BaseMessageTile({entry, roomVM: {room}, timeline, platform: {logger}});
|
return new BaseMessageTile(entry, {roomVM: {room}, timeline, platform: {logger}});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, (tile, params, entry) => tile?.updateEntry(entry, params, function () {}));
|
}, (tile, params, entry) => tile?.updateEntry(entry, params, function () {}));
|
||||||
|
|
|
@ -18,20 +18,27 @@ import {BaseObservableList} from "../../../../observable/list/BaseObservableList
|
||||||
import {sortedIndex} from "../../../../utils/sortedIndex";
|
import {sortedIndex} from "../../../../utils/sortedIndex";
|
||||||
|
|
||||||
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary
|
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary
|
||||||
// for now, tileCreator should be stable in whether it returns a tile or not.
|
// for now, tileClassForEntry should be stable in whether it returns a tile or not.
|
||||||
// e.g. the decision to create a tile or not should be based on properties
|
// e.g. the decision to create a tile or not should be based on properties
|
||||||
// not updated later on (e.g. event type)
|
// not updated later on (e.g. event type)
|
||||||
// also see big comment in onUpdate
|
// also see big comment in onUpdate
|
||||||
export class TilesCollection extends BaseObservableList {
|
export class TilesCollection extends BaseObservableList {
|
||||||
constructor(entries, tileCreator) {
|
constructor(entries, tileOptions) {
|
||||||
super();
|
super();
|
||||||
this._entries = entries;
|
this._entries = entries;
|
||||||
this._tiles = null;
|
this._tiles = null;
|
||||||
this._entrySubscription = null;
|
this._entrySubscription = null;
|
||||||
this._tileCreator = tileCreator;
|
this._tileOptions = tileOptions;
|
||||||
this._emitSpontanousUpdate = this._emitSpontanousUpdate.bind(this);
|
this._emitSpontanousUpdate = this._emitSpontanousUpdate.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createTile(entry) {
|
||||||
|
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||||
|
if (Tile) {
|
||||||
|
return new Tile(entry, this._tileOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_emitSpontanousUpdate(tile, params) {
|
_emitSpontanousUpdate(tile, params) {
|
||||||
const entry = tile.lowerEntry;
|
const entry = tile.lowerEntry;
|
||||||
const tileIdx = this._findTileIdx(entry);
|
const tileIdx = this._findTileIdx(entry);
|
||||||
|
@ -48,7 +55,7 @@ export class TilesCollection extends BaseObservableList {
|
||||||
let currentTile = null;
|
let currentTile = null;
|
||||||
for (let entry of this._entries) {
|
for (let entry of this._entries) {
|
||||||
if (!currentTile || !currentTile.tryIncludeEntry(entry)) {
|
if (!currentTile || !currentTile.tryIncludeEntry(entry)) {
|
||||||
currentTile = this._tileCreator(entry);
|
currentTile = this._createTile(entry);
|
||||||
if (currentTile) {
|
if (currentTile) {
|
||||||
this._tiles.push(currentTile);
|
this._tiles.push(currentTile);
|
||||||
}
|
}
|
||||||
|
@ -121,7 +128,7 @@ export class TilesCollection extends BaseObservableList {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTile = this._tileCreator(entry);
|
const newTile = this._createTile(entry);
|
||||||
if (newTile) {
|
if (newTile) {
|
||||||
if (prevTile) {
|
if (prevTile) {
|
||||||
prevTile.updateNextSibling(newTile);
|
prevTile.updateNextSibling(newTile);
|
||||||
|
@ -150,9 +157,9 @@ export class TilesCollection extends BaseObservableList {
|
||||||
const tileIdx = this._findTileIdx(entry);
|
const tileIdx = this._findTileIdx(entry);
|
||||||
const tile = this._findTileAtIdx(entry, tileIdx);
|
const tile = this._findTileAtIdx(entry, tileIdx);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
const action = tile.updateEntry(entry, params, this._tileCreator);
|
const action = tile.updateEntry(entry, params);
|
||||||
if (action.shouldReplace) {
|
if (action.shouldReplace) {
|
||||||
const newTile = this._tileCreator(entry);
|
const newTile = this._createTile(entry);
|
||||||
if (newTile) {
|
if (newTile) {
|
||||||
this._replaceTile(tileIdx, tile, newTile, action.updateParams);
|
this._replaceTile(tileIdx, tile, newTile, action.updateParams);
|
||||||
newTile.setUpdateEmit(this._emitSpontanousUpdate);
|
newTile.setUpdateEmit(this._emitSpontanousUpdate);
|
||||||
|
@ -303,7 +310,10 @@ export function tests() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const entries = new ObservableArray([{n: 5}, {n: 10}]);
|
const entries = new ObservableArray([{n: 5}, {n: 10}]);
|
||||||
const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry));
|
const tileOptions = {
|
||||||
|
tileClassForEntry: () => UpdateOnSiblingTile,
|
||||||
|
};
|
||||||
|
const tiles = new TilesCollection(entries, tileOptions);
|
||||||
let receivedAdd = false;
|
let receivedAdd = false;
|
||||||
tiles.subscribe({
|
tiles.subscribe({
|
||||||
onAdd(idx, tile) {
|
onAdd(idx, tile) {
|
||||||
|
@ -326,7 +336,10 @@ export function tests() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const entries = new ObservableArray([{n: 5}, {n: 10}, {n: 15}]);
|
const entries = new ObservableArray([{n: 5}, {n: 10}, {n: 15}]);
|
||||||
const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry));
|
const tileOptions = {
|
||||||
|
tileClassForEntry: () => UpdateOnSiblingTile,
|
||||||
|
};
|
||||||
|
const tiles = new TilesCollection(entries, tileOptions);
|
||||||
const events = [];
|
const events = [];
|
||||||
tiles.subscribe({
|
tiles.subscribe({
|
||||||
onUpdate(idx, tile) {
|
onUpdate(idx, tile) {
|
||||||
|
|
|
@ -37,9 +37,9 @@ import {ViewModel} from "../../../ViewModel";
|
||||||
export class TimelineViewModel extends ViewModel {
|
export class TimelineViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {timeline, tilesCreator} = options;
|
const {timeline, tileOptions} = options;
|
||||||
this._timeline = this.track(timeline);
|
this._timeline = this.track(timeline);
|
||||||
this._tiles = new TilesCollection(timeline.entries, tilesCreator);
|
this._tiles = new TilesCollection(timeline.entries, tileOptions);
|
||||||
this._startTile = null;
|
this._startTile = null;
|
||||||
this._endTile = null;
|
this._endTile = null;
|
||||||
this._topLoadingPromise = null;
|
this._topLoadingPromise = null;
|
||||||
|
|
|
@ -21,8 +21,8 @@ const MAX_HEIGHT = 300;
|
||||||
const MAX_WIDTH = 400;
|
const MAX_WIDTH = 400;
|
||||||
|
|
||||||
export class BaseMediaTile extends BaseMessageTile {
|
export class BaseMediaTile extends BaseMessageTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._decryptedThumbnail = null;
|
this._decryptedThumbnail = null;
|
||||||
this._decryptedFile = null;
|
this._decryptedFile = null;
|
||||||
this._isVisible = false;
|
this._isVisible = false;
|
||||||
|
|
|
@ -19,8 +19,8 @@ import {ReactionsViewModel} from "../ReactionsViewModel.js";
|
||||||
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
|
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
|
||||||
|
|
||||||
export class BaseMessageTile extends SimpleTile {
|
export class BaseMessageTile extends SimpleTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||||
this._isContinuation = false;
|
this._isContinuation = false;
|
||||||
this._reactions = null;
|
this._reactions = null;
|
||||||
|
@ -28,7 +28,7 @@ export class BaseMessageTile extends SimpleTile {
|
||||||
if (this._entry.annotations || this._entry.pendingAnnotations) {
|
if (this._entry.annotations || this._entry.pendingAnnotations) {
|
||||||
this._updateReactions();
|
this._updateReactions();
|
||||||
}
|
}
|
||||||
this._updateReplyTileIfNeeded(options.tilesCreator, undefined);
|
this._updateReplyTileIfNeeded(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyVisible() {
|
notifyVisible() {
|
||||||
|
@ -114,23 +114,27 @@ export class BaseMessageTile extends SimpleTile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEntry(entry, param, tilesCreator) {
|
updateEntry(entry, param) {
|
||||||
const action = super.updateEntry(entry, param, tilesCreator);
|
const action = super.updateEntry(entry, param);
|
||||||
if (action.shouldUpdate) {
|
if (action.shouldUpdate) {
|
||||||
this._updateReactions();
|
this._updateReactions();
|
||||||
}
|
}
|
||||||
this._updateReplyTileIfNeeded(tilesCreator, param);
|
this._updateReplyTileIfNeeded(param);
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateReplyTileIfNeeded(tilesCreator, param) {
|
_updateReplyTileIfNeeded(param) {
|
||||||
const replyEntry = this._entry.contextEntry;
|
const replyEntry = this._entry.contextEntry;
|
||||||
if (replyEntry) {
|
if (replyEntry) {
|
||||||
// this is an update to contextEntry used for replyPreview
|
// this is an update to contextEntry used for replyPreview
|
||||||
const action = this._replyTile?.updateEntry(replyEntry, param, tilesCreator);
|
const action = this._replyTile?.updateEntry(replyEntry, param);
|
||||||
if (action?.shouldReplace || !this._replyTile) {
|
if (action?.shouldReplace || !this._replyTile) {
|
||||||
this.disposeTracked(this._replyTile);
|
this.disposeTracked(this._replyTile);
|
||||||
this._replyTile = tilesCreator(replyEntry);
|
const tileClassForEntry = this._options.tileClassForEntry;
|
||||||
|
const ReplyTile = tileClassForEntry(replyEntry);
|
||||||
|
if (ReplyTile) {
|
||||||
|
this._replyTile = new ReplyTile(replyEntry, this._options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(action?.shouldUpdate) {
|
if(action?.shouldUpdate) {
|
||||||
this._replyTile?.emitChange();
|
this._replyTile?.emitChange();
|
||||||
|
|
|
@ -21,8 +21,8 @@ import {createEnum} from "../../../../../utils/enum";
|
||||||
export const BodyFormat = createEnum("Plain", "Html");
|
export const BodyFormat = createEnum("Plain", "Html");
|
||||||
|
|
||||||
export class BaseTextTile extends BaseMessageTile {
|
export class BaseTextTile extends BaseMessageTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._messageBody = null;
|
this._messageBody = null;
|
||||||
this._format = null
|
this._format = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,8 @@ import {LocalMedia} from "../../../../../matrix/calls/LocalMedia";
|
||||||
// alternatively, we could just subscribe to the GroupCall and spontanously emit an update when it updates
|
// alternatively, we could just subscribe to the GroupCall and spontanously emit an update when it updates
|
||||||
|
|
||||||
export class CallTile extends SimpleTile {
|
export class CallTile extends SimpleTile {
|
||||||
|
constructor(entry, options) {
|
||||||
constructor(options) {
|
super(entry, options);
|
||||||
super(options);
|
|
||||||
const calls = this.getOption("session").callHandler.calls;
|
const calls = this.getOption("session").callHandler.calls;
|
||||||
this._call = calls.get(this._entry.stateKey);
|
this._call = calls.get(this._entry.stateKey);
|
||||||
this._callSubscription = undefined;
|
this._callSubscription = undefined;
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {BaseTextTile} from "./BaseTextTile.js";
|
||||||
import {UpdateAction} from "../UpdateAction.js";
|
import {UpdateAction} from "../UpdateAction.js";
|
||||||
|
|
||||||
export class EncryptedEventTile extends BaseTextTile {
|
export class EncryptedEventTile extends BaseTextTile {
|
||||||
updateEntry(entry, params, tilesCreator) {
|
updateEntry(entry, params) {
|
||||||
const parentResult = super.updateEntry(entry, params, tilesCreator);
|
const parentResult = super.updateEntry(entry, params);
|
||||||
// event got decrypted, recreate the tile and replace this one with it
|
// event got decrypted, recreate the tile and replace this one with it
|
||||||
if (entry.eventType !== "m.room.encrypted") {
|
if (entry.eventType !== "m.room.encrypted") {
|
||||||
// the "shape" parameter trigger tile recreation in TimelineView
|
// the "shape" parameter trigger tile recreation in TimelineView
|
||||||
|
|
|
@ -20,8 +20,8 @@ import {formatSize} from "../../../../../utils/formatSize";
|
||||||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||||
|
|
||||||
export class FileTile extends BaseMessageTile {
|
export class FileTile extends BaseMessageTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._downloadError = null;
|
this._downloadError = null;
|
||||||
this._downloading = false;
|
this._downloading = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {SimpleTile} from "./SimpleTile.js";
|
||||||
import {UpdateAction} from "../UpdateAction.js";
|
import {UpdateAction} from "../UpdateAction.js";
|
||||||
|
|
||||||
export class GapTile extends SimpleTile {
|
export class GapTile extends SimpleTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this._isAtTop = true;
|
this._isAtTop = true;
|
||||||
|
@ -81,8 +81,8 @@ export class GapTile extends SimpleTile {
|
||||||
this._siblingChanged = true;
|
this._siblingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEntry(entry, params, tilesCreator) {
|
updateEntry(entry, params) {
|
||||||
super.updateEntry(entry, params, tilesCreator);
|
super.updateEntry(entry, params);
|
||||||
if (!entry.isGap) {
|
if (!entry.isGap) {
|
||||||
return UpdateAction.Remove();
|
return UpdateAction.Remove();
|
||||||
} else {
|
} else {
|
||||||
|
@ -125,7 +125,7 @@ export function tests() {
|
||||||
tile.updateEntry(newEntry);
|
tile.updateEntry(newEntry);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tile = new GapTile({entry: new FragmentBoundaryEntry(fragment, true), roomVM: {room}});
|
const tile = new GapTile(new FragmentBoundaryEntry(fragment, true), {roomVM: {room}});
|
||||||
await tile.fill();
|
await tile.fill();
|
||||||
await tile.fill();
|
await tile.fill();
|
||||||
await tile.fill();
|
await tile.fill();
|
||||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||||
import {BaseMediaTile} from "./BaseMediaTile.js";
|
import {BaseMediaTile} from "./BaseMediaTile.js";
|
||||||
|
|
||||||
export class ImageTile extends BaseMediaTile {
|
export class ImageTile extends BaseMediaTile {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(entry, options);
|
||||||
this._lightboxUrl = this.urlCreator.urlForSegments([
|
this._lightboxUrl = this.urlCreator.urlForSegments([
|
||||||
// ensure the right room is active if in grid view
|
// ensure the right room is active if in grid view
|
||||||
this.navigation.segment("room", this._room.id),
|
this.navigation.segment("room", this._room.id),
|
||||||
|
|
|
@ -66,23 +66,25 @@ export class RoomMemberTile extends SimpleTile {
|
||||||
export function tests() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
"user removes display name": (assert) => {
|
"user removes display name": (assert) => {
|
||||||
const tile = new RoomMemberTile({
|
const tile = new RoomMemberTile(
|
||||||
entry: {
|
{
|
||||||
prevContent: {displayname: "foo", membership: "join"},
|
prevContent: {displayname: "foo", membership: "join"},
|
||||||
content: {membership: "join"},
|
content: {membership: "join"},
|
||||||
stateKey: "foo@bar.com",
|
stateKey: "foo@bar.com",
|
||||||
},
|
},
|
||||||
});
|
{}
|
||||||
|
);
|
||||||
assert.strictEqual(tile.announcement, "foo@bar.com removed their name (foo)");
|
assert.strictEqual(tile.announcement, "foo@bar.com removed their name (foo)");
|
||||||
},
|
},
|
||||||
"user without display name sets a new display name": (assert) => {
|
"user without display name sets a new display name": (assert) => {
|
||||||
const tile = new RoomMemberTile({
|
const tile = new RoomMemberTile(
|
||||||
entry: {
|
{
|
||||||
prevContent: {membership: "join"},
|
prevContent: {membership: "join"},
|
||||||
content: {displayname: "foo", membership: "join" },
|
content: {displayname: "foo", membership: "join" },
|
||||||
stateKey: "foo@bar.com",
|
stateKey: "foo@bar.com",
|
||||||
},
|
},
|
||||||
});
|
{}
|
||||||
|
);
|
||||||
assert.strictEqual(tile.announcement, "foo@bar.com changed their name to foo");
|
assert.strictEqual(tile.announcement, "foo@bar.com changed their name to foo");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,9 +19,9 @@ import {ViewModel} from "../../../../ViewModel";
|
||||||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||||
|
|
||||||
export class SimpleTile extends ViewModel {
|
export class SimpleTile extends ViewModel {
|
||||||
constructor(options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._entry = options.entry;
|
this._entry = entry;
|
||||||
}
|
}
|
||||||
// view model props for all subclasses
|
// view model props for all subclasses
|
||||||
// hmmm, could also do instanceof ... ?
|
// hmmm, could also do instanceof ... ?
|
||||||
|
|
105
src/domain/session/room/timeline/tiles/index.ts
Normal file
105
src/domain/session/room/timeline/tiles/index.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {GapTile} from "./GapTile.js";
|
||||||
|
import {TextTile} from "./TextTile.js";
|
||||||
|
import {RedactedTile} from "./RedactedTile.js";
|
||||||
|
import {ImageTile} from "./ImageTile.js";
|
||||||
|
import {VideoTile} from "./VideoTile.js";
|
||||||
|
import {FileTile} from "./FileTile.js";
|
||||||
|
import {LocationTile} from "./LocationTile.js";
|
||||||
|
import {RoomNameTile} from "./RoomNameTile.js";
|
||||||
|
import {RoomMemberTile} from "./RoomMemberTile.js";
|
||||||
|
import {EncryptedEventTile} from "./EncryptedEventTile.js";
|
||||||
|
import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js";
|
||||||
|
import {MissingAttachmentTile} from "./MissingAttachmentTile.js";
|
||||||
|
import {CallTile} from "./CallTile.js";
|
||||||
|
|
||||||
|
import type {SimpleTile} from "./SimpleTile.js";
|
||||||
|
import type {Room} from "../../../../../matrix/room/Room";
|
||||||
|
import type {Session} from "../../../../../matrix/Session";
|
||||||
|
import type {Timeline} from "../../../../../matrix/room/timeline/Timeline";
|
||||||
|
import type {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry";
|
||||||
|
import type {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry";
|
||||||
|
import type {PendingEventEntry} from "../../../../../matrix/room/timeline/entries/PendingEventEntry";
|
||||||
|
import type {Options as ViewModelOptions} from "../../../../ViewModel";
|
||||||
|
|
||||||
|
export type TimelineEntry = FragmentBoundaryEntry | EventEntry | PendingEventEntry;
|
||||||
|
export type TileClassForEntryFn = (entry: TimelineEntry) => TileConstructor | undefined;
|
||||||
|
export type Options = ViewModelOptions & {
|
||||||
|
session: Session,
|
||||||
|
room: Room,
|
||||||
|
timeline: Timeline
|
||||||
|
tileClassForEntry: TileClassForEntryFn;
|
||||||
|
};
|
||||||
|
export type TileConstructor = new (entry: TimelineEntry, options: Options) => SimpleTile;
|
||||||
|
|
||||||
|
export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined {
|
||||||
|
if (entry.isGap) {
|
||||||
|
return GapTile;
|
||||||
|
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
||||||
|
return MissingAttachmentTile;
|
||||||
|
} else if (entry.eventType) {
|
||||||
|
switch (entry.eventType) {
|
||||||
|
case "m.room.message": {
|
||||||
|
if (entry.isRedacted) {
|
||||||
|
return RedactedTile;
|
||||||
|
}
|
||||||
|
const content = entry.content;
|
||||||
|
const msgtype = content && content.msgtype;
|
||||||
|
switch (msgtype) {
|
||||||
|
case "m.text":
|
||||||
|
case "m.notice":
|
||||||
|
case "m.emote":
|
||||||
|
return TextTile;
|
||||||
|
case "m.image":
|
||||||
|
return ImageTile;
|
||||||
|
case "m.video":
|
||||||
|
return VideoTile;
|
||||||
|
case "m.file":
|
||||||
|
return FileTile;
|
||||||
|
case "m.location":
|
||||||
|
return LocationTile;
|
||||||
|
default:
|
||||||
|
// unknown msgtype not rendered
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "m.room.name":
|
||||||
|
return RoomNameTile;
|
||||||
|
case "m.room.member":
|
||||||
|
return RoomMemberTile;
|
||||||
|
case "m.room.encrypted":
|
||||||
|
if (entry.isRedacted) {
|
||||||
|
return RedactedTile;
|
||||||
|
}
|
||||||
|
return EncryptedEventTile;
|
||||||
|
case "m.room.encryption":
|
||||||
|
return EncryptionEnabledTile;
|
||||||
|
case "org.matrix.msc3401.call": {
|
||||||
|
// if prevContent is present, it's an update to a call event, which we don't render
|
||||||
|
// as the original event is updated through the call object which receive state event updates
|
||||||
|
if (entry.stateKey && !entry.prevContent) {
|
||||||
|
return CallTile;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown type not rendered
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {GapTile} from "./tiles/GapTile.js";
|
|
||||||
import {TextTile} from "./tiles/TextTile.js";
|
|
||||||
import {RedactedTile} from "./tiles/RedactedTile.js";
|
|
||||||
import {ImageTile} from "./tiles/ImageTile.js";
|
|
||||||
import {VideoTile} from "./tiles/VideoTile.js";
|
|
||||||
import {FileTile} from "./tiles/FileTile.js";
|
|
||||||
import {LocationTile} from "./tiles/LocationTile.js";
|
|
||||||
import {RoomNameTile} from "./tiles/RoomNameTile.js";
|
|
||||||
import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
|
||||||
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
|
|
||||||
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
|
|
||||||
import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
|
|
||||||
import {CallTile} from "./tiles/CallTile.js";
|
|
||||||
|
|
||||||
export function tilesCreator(baseOptions) {
|
|
||||||
const tilesCreator = function tilesCreator(entry, emitUpdate) {
|
|
||||||
const options = Object.assign({entry, emitUpdate, tilesCreator}, baseOptions);
|
|
||||||
if (entry.isGap) {
|
|
||||||
return new GapTile(options);
|
|
||||||
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
|
||||||
return new MissingAttachmentTile(options);
|
|
||||||
} else if (entry.eventType) {
|
|
||||||
switch (entry.eventType) {
|
|
||||||
case "m.room.message": {
|
|
||||||
if (entry.isRedacted) {
|
|
||||||
return new RedactedTile(options);
|
|
||||||
}
|
|
||||||
const content = entry.content;
|
|
||||||
const msgtype = content && content.msgtype;
|
|
||||||
switch (msgtype) {
|
|
||||||
case "m.text":
|
|
||||||
case "m.notice":
|
|
||||||
case "m.emote":
|
|
||||||
return new TextTile(options);
|
|
||||||
case "m.image":
|
|
||||||
return new ImageTile(options);
|
|
||||||
case "m.video":
|
|
||||||
return new VideoTile(options);
|
|
||||||
case "m.file":
|
|
||||||
return new FileTile(options);
|
|
||||||
case "m.location":
|
|
||||||
return new LocationTile(options);
|
|
||||||
default:
|
|
||||||
// unknown msgtype not rendered
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "m.room.name":
|
|
||||||
return new RoomNameTile(options);
|
|
||||||
case "m.room.member":
|
|
||||||
return new RoomMemberTile(options);
|
|
||||||
case "m.room.encrypted":
|
|
||||||
if (entry.isRedacted) {
|
|
||||||
return new RedactedTile(options);
|
|
||||||
}
|
|
||||||
return new EncryptedEventTile(options);
|
|
||||||
case "m.room.encryption":
|
|
||||||
return new EncryptionEnabledTile(options);
|
|
||||||
case "org.matrix.msc3401.call":
|
|
||||||
// if prevContent is present, it's an update to a call event, which we don't render
|
|
||||||
// as the original event is updated through the call object which receive state event updates
|
|
||||||
return entry.stateKey && !entry.prevContent ? new CallTile(options) : null;
|
|
||||||
default:
|
|
||||||
// unknown type not rendered
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return tilesCreator;
|
|
||||||
}
|
|
34
src/lib.ts
34
src/lib.ts
|
@ -26,7 +26,41 @@ export {SessionView} from "./platform/web/ui/session/SessionView.js";
|
||||||
export {RoomViewModel} from "./domain/session/room/RoomViewModel.js";
|
export {RoomViewModel} from "./domain/session/room/RoomViewModel.js";
|
||||||
export {RoomView} from "./platform/web/ui/session/room/RoomView.js";
|
export {RoomView} from "./platform/web/ui/session/room/RoomView.js";
|
||||||
export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js";
|
export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js";
|
||||||
|
export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index";
|
||||||
|
export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index";
|
||||||
|
// export timeline tile view models
|
||||||
|
export {GapTile} from "./domain/session/room/timeline/tiles/GapTile.js";
|
||||||
|
export {TextTile} from "./domain/session/room/timeline/tiles/TextTile.js";
|
||||||
|
export {RedactedTile} from "./domain/session/room/timeline/tiles/RedactedTile.js";
|
||||||
|
export {ImageTile} from "./domain/session/room/timeline/tiles/ImageTile.js";
|
||||||
|
export {VideoTile} from "./domain/session/room/timeline/tiles/VideoTile.js";
|
||||||
|
export {FileTile} from "./domain/session/room/timeline/tiles/FileTile.js";
|
||||||
|
export {LocationTile} from "./domain/session/room/timeline/tiles/LocationTile.js";
|
||||||
|
export {RoomNameTile} from "./domain/session/room/timeline/tiles/RoomNameTile.js";
|
||||||
|
export {RoomMemberTile} from "./domain/session/room/timeline/tiles/RoomMemberTile.js";
|
||||||
|
export {EncryptedEventTile} from "./domain/session/room/timeline/tiles/EncryptedEventTile.js";
|
||||||
|
export {EncryptionEnabledTile} from "./domain/session/room/timeline/tiles/EncryptionEnabledTile.js";
|
||||||
|
export {MissingAttachmentTile} from "./domain/session/room/timeline/tiles/MissingAttachmentTile.js";
|
||||||
|
export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile.js";
|
||||||
|
|
||||||
export {TimelineView} from "./platform/web/ui/session/room/TimelineView";
|
export {TimelineView} from "./platform/web/ui/session/room/TimelineView";
|
||||||
|
export {viewClassForTile} from "./platform/web/ui/session/room/common";
|
||||||
|
export type {TileViewConstructor, ViewClassForEntryFn} from "./platform/web/ui/session/room/TimelineView";
|
||||||
|
// export timeline tile views
|
||||||
|
export {AnnouncementView} from "./platform/web/ui/session/room/timeline/AnnouncementView.js";
|
||||||
|
export {BaseMediaView} from "./platform/web/ui/session/room/timeline/BaseMediaView.js";
|
||||||
|
export {BaseMessageView} from "./platform/web/ui/session/room/timeline/BaseMessageView.js";
|
||||||
|
export {FileView} from "./platform/web/ui/session/room/timeline/FileView.js";
|
||||||
|
export {GapView} from "./platform/web/ui/session/room/timeline/GapView.js";
|
||||||
|
export {ImageView} from "./platform/web/ui/session/room/timeline/ImageView.js";
|
||||||
|
export {LocationView} from "./platform/web/ui/session/room/timeline/LocationView.js";
|
||||||
|
export {MissingAttachmentView} from "./platform/web/ui/session/room/timeline/MissingAttachmentView.js";
|
||||||
|
export {ReactionsView} from "./platform/web/ui/session/room/timeline/ReactionsView.js";
|
||||||
|
export {RedactedView} from "./platform/web/ui/session/room/timeline/RedactedView.js";
|
||||||
|
export {ReplyPreviewView} from "./platform/web/ui/session/room/timeline/ReplyPreviewView.js";
|
||||||
|
export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessageView.js";
|
||||||
|
export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js";
|
||||||
|
|
||||||
export {Navigation} from "./domain/navigation/Navigation.js";
|
export {Navigation} from "./domain/navigation/Navigation.js";
|
||||||
export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js";
|
export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js";
|
||||||
export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js";
|
export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js";
|
||||||
|
|
|
@ -21,6 +21,11 @@ import {TemplateView} from "../general/TemplateView";
|
||||||
import {StaticView} from "../general/StaticView.js";
|
import {StaticView} from "../general/StaticView.js";
|
||||||
|
|
||||||
export class RoomGridView extends TemplateView {
|
export class RoomGridView extends TemplateView {
|
||||||
|
constructor(vm, viewClassForTile) {
|
||||||
|
super(vm);
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
|
}
|
||||||
|
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const children = [];
|
const children = [];
|
||||||
for (let i = 0; i < (vm.height * vm.width); i+=1) {
|
for (let i = 0; i < (vm.height * vm.width); i+=1) {
|
||||||
|
@ -39,7 +44,7 @@ export class RoomGridView extends TemplateView {
|
||||||
} else if (roomVM.kind === "invite") {
|
} else if (roomVM.kind === "invite") {
|
||||||
return new InviteView(roomVM);
|
return new InviteView(roomVM);
|
||||||
} else {
|
} else {
|
||||||
return new RoomView(roomVM);
|
return new RoomView(roomVM, this._viewClassForTile);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new StaticView(t => t.div({className: "room-placeholder"}, [
|
return new StaticView(t => t.div({className: "room-placeholder"}, [
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {RoomGridView} from "./RoomGridView.js";
|
||||||
import {SettingsView} from "./settings/SettingsView.js";
|
import {SettingsView} from "./settings/SettingsView.js";
|
||||||
import {CreateRoomView} from "./CreateRoomView.js";
|
import {CreateRoomView} from "./CreateRoomView.js";
|
||||||
import {RightPanelView} from "./rightpanel/RightPanelView.js";
|
import {RightPanelView} from "./rightpanel/RightPanelView.js";
|
||||||
|
import {viewClassForTile} from "./room/common";
|
||||||
|
|
||||||
export class SessionView extends TemplateView {
|
export class SessionView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
|
@ -42,7 +43,7 @@ export class SessionView extends TemplateView {
|
||||||
t.view(new LeftPanelView(vm.leftPanelViewModel)),
|
t.view(new LeftPanelView(vm.leftPanelViewModel)),
|
||||||
t.mapView(vm => vm.activeMiddleViewModel, () => {
|
t.mapView(vm => vm.activeMiddleViewModel, () => {
|
||||||
if (vm.roomGridViewModel) {
|
if (vm.roomGridViewModel) {
|
||||||
return new RoomGridView(vm.roomGridViewModel);
|
return new RoomGridView(vm.roomGridViewModel, viewClassForTile);
|
||||||
} else if (vm.settingsViewModel) {
|
} else if (vm.settingsViewModel) {
|
||||||
return new SettingsView(vm.settingsViewModel);
|
return new SettingsView(vm.settingsViewModel);
|
||||||
} else if (vm.createRoomViewModel) {
|
} else if (vm.createRoomViewModel) {
|
||||||
|
@ -51,7 +52,7 @@ export class SessionView extends TemplateView {
|
||||||
if (vm.currentRoomViewModel.kind === "invite") {
|
if (vm.currentRoomViewModel.kind === "invite") {
|
||||||
return new InviteView(vm.currentRoomViewModel);
|
return new InviteView(vm.currentRoomViewModel);
|
||||||
} else if (vm.currentRoomViewModel.kind === "room") {
|
} else if (vm.currentRoomViewModel.kind === "room") {
|
||||||
return new RoomView(vm.currentRoomViewModel);
|
return new RoomView(vm.currentRoomViewModel, viewClassForTile);
|
||||||
} else if (vm.currentRoomViewModel.kind === "roomBeingCreated") {
|
} else if (vm.currentRoomViewModel.kind === "roomBeingCreated") {
|
||||||
return new RoomBeingCreatedView(vm.currentRoomViewModel);
|
return new RoomBeingCreatedView(vm.currentRoomViewModel);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,11 +17,11 @@ limitations under the License.
|
||||||
import {TemplateView} from "../../general/TemplateView";
|
import {TemplateView} from "../../general/TemplateView";
|
||||||
import {Popup} from "../../general/Popup.js";
|
import {Popup} from "../../general/Popup.js";
|
||||||
import {Menu} from "../../general/Menu.js";
|
import {Menu} from "../../general/Menu.js";
|
||||||
import {viewClassForEntry} from "./common"
|
|
||||||
|
|
||||||
export class MessageComposer extends TemplateView {
|
export class MessageComposer extends TemplateView {
|
||||||
constructor(viewModel) {
|
constructor(viewModel, viewClassForTile) {
|
||||||
super(viewModel);
|
super(viewModel);
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
this._input = null;
|
this._input = null;
|
||||||
this._attachmentPopup = null;
|
this._attachmentPopup = null;
|
||||||
this._focusInput = null;
|
this._focusInput = null;
|
||||||
|
@ -45,8 +45,8 @@ export class MessageComposer extends TemplateView {
|
||||||
this._focusInput = () => this._input.focus();
|
this._focusInput = () => this._input.focus();
|
||||||
this.value.on("focus", this._focusInput);
|
this.value.on("focus", this._focusInput);
|
||||||
const replyPreview = t.map(vm => vm.replyViewModel, (rvm, t) => {
|
const replyPreview = t.map(vm => vm.replyViewModel, (rvm, t) => {
|
||||||
const View = rvm && viewClassForEntry(rvm);
|
const TileView = rvm && this._viewClassForTile(rvm);
|
||||||
if (!View) { return null; }
|
if (!TileView) { return null; }
|
||||||
return t.div({
|
return t.div({
|
||||||
className: "MessageComposer_replyPreview"
|
className: "MessageComposer_replyPreview"
|
||||||
}, [
|
}, [
|
||||||
|
@ -55,8 +55,8 @@ export class MessageComposer extends TemplateView {
|
||||||
className: "cancel",
|
className: "cancel",
|
||||||
onClick: () => this._clearReplyingTo()
|
onClick: () => this._clearReplyingTo()
|
||||||
}, "Close"),
|
}, "Close"),
|
||||||
t.view(new View(rvm, { interactive: false }, "div"))
|
t.view(new TileView(rvm, this._viewClassForTile, { interactive: false }, "div"))
|
||||||
])
|
]);
|
||||||
});
|
});
|
||||||
const input = t.div({className: "MessageComposer_input"}, [
|
const input = t.div({className: "MessageComposer_input"}, [
|
||||||
this._input,
|
this._input,
|
||||||
|
|
|
@ -26,15 +26,16 @@ import {AvatarView} from "../../AvatarView.js";
|
||||||
import {CallView} from "./CallView";
|
import {CallView} from "./CallView";
|
||||||
|
|
||||||
export class RoomView extends TemplateView {
|
export class RoomView extends TemplateView {
|
||||||
constructor(options) {
|
constructor(vm, viewClassForTile) {
|
||||||
super(options);
|
super(vm);
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
this._optionsPopup = null;
|
this._optionsPopup = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
let bottomView;
|
let bottomView;
|
||||||
if (vm.composerViewModel.kind === "composer") {
|
if (vm.composerViewModel.kind === "composer") {
|
||||||
bottomView = new MessageComposer(vm.composerViewModel);
|
bottomView = new MessageComposer(vm.composerViewModel, this._viewClassForTile);
|
||||||
} else if (vm.composerViewModel.kind === "archived") {
|
} else if (vm.composerViewModel.kind === "archived") {
|
||||||
bottomView = new RoomArchivedView(vm.composerViewModel);
|
bottomView = new RoomArchivedView(vm.composerViewModel);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,7 @@ export class RoomView extends TemplateView {
|
||||||
t.mapView(vm => vm.callViewModel, callViewModel => callViewModel ? new CallView(callViewModel) : null),
|
t.mapView(vm => vm.callViewModel, callViewModel => callViewModel ? new CallView(callViewModel) : null),
|
||||||
t.mapView(vm => vm.timelineViewModel, timelineViewModel => {
|
t.mapView(vm => vm.timelineViewModel, timelineViewModel => {
|
||||||
return timelineViewModel ?
|
return timelineViewModel ?
|
||||||
new TimelineView(timelineViewModel) :
|
new TimelineView(timelineViewModel, this._viewClassForTile) :
|
||||||
new TimelineLoadingView(vm); // vm is just needed for i18n
|
new TimelineLoadingView(vm); // vm is just needed for i18n
|
||||||
}),
|
}),
|
||||||
t.view(bottomView),
|
t.view(bottomView),
|
||||||
|
|
|
@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {TileView} from "./common";
|
|
||||||
import {viewClassForEntry} from "./common";
|
|
||||||
import {ListView} from "../../general/ListView";
|
import {ListView} from "../../general/ListView";
|
||||||
|
import type {IView} from "../../general/types";
|
||||||
import {TemplateView, Builder} from "../../general/TemplateView";
|
import {TemplateView, Builder} from "../../general/TemplateView";
|
||||||
import {IObservableValue} from "../../general/BaseUpdateView";
|
import {IObservableValue} from "../../general/BaseUpdateView";
|
||||||
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
||||||
|
@ -25,6 +24,17 @@ import {RedactedView} from "./timeline/RedactedView.js";
|
||||||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||||
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
|
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
|
||||||
|
|
||||||
|
export interface TileView extends IView {
|
||||||
|
readonly value: SimpleTile;
|
||||||
|
onClick(event: UIEvent);
|
||||||
|
}
|
||||||
|
export type TileViewConstructor = new (
|
||||||
|
tile: SimpleTile,
|
||||||
|
viewClassForTile: ViewClassForEntryFn,
|
||||||
|
renderFlags?: { reply?: boolean, interactive?: boolean }
|
||||||
|
) => TileView;
|
||||||
|
export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor;
|
||||||
|
|
||||||
//import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
|
//import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
|
||||||
export interface TimelineViewModel extends IObservableValue {
|
export interface TimelineViewModel extends IObservableValue {
|
||||||
showJumpDown: boolean;
|
showJumpDown: boolean;
|
||||||
|
@ -55,13 +65,17 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
||||||
private tilesView?: TilesListView;
|
private tilesView?: TilesListView;
|
||||||
private resizeObserver?: ResizeObserver;
|
private resizeObserver?: ResizeObserver;
|
||||||
|
|
||||||
|
constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) {
|
||||||
|
super(vm);
|
||||||
|
}
|
||||||
|
|
||||||
render(t: Builder<TimelineViewModel>, vm: TimelineViewModel) {
|
render(t: Builder<TimelineViewModel>, vm: TimelineViewModel) {
|
||||||
// assume this view will be mounted in the parent DOM straight away
|
// assume this view will be mounted in the parent DOM straight away
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// do initial scroll positioning
|
// do initial scroll positioning
|
||||||
this.restoreScrollPosition();
|
this.restoreScrollPosition();
|
||||||
});
|
});
|
||||||
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
|
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition(), this.viewClassForTile);
|
||||||
const root = t.div({className: "Timeline"}, [
|
const root = t.div({className: "Timeline"}, [
|
||||||
t.div({
|
t.div({
|
||||||
className: "Timeline_scroller bottom-aligned-scroll",
|
className: "Timeline_scroller bottom-aligned-scroll",
|
||||||
|
@ -174,16 +188,13 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
||||||
|
|
||||||
private onChanged: () => void;
|
private onChanged: () => void;
|
||||||
|
|
||||||
constructor(tiles: ObservableList<SimpleTile>, onChanged: () => void) {
|
constructor(tiles: ObservableList<SimpleTile>, onChanged: () => void, private readonly viewClassForTile: ViewClassForEntryFn) {
|
||||||
const options = {
|
super({
|
||||||
list: tiles,
|
list: tiles,
|
||||||
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
||||||
};
|
}, tile => {
|
||||||
super(options, entry => {
|
const TileView = viewClassForTile(tile);
|
||||||
const View = viewClassForEntry(entry);
|
return new TileView(tile, viewClassForTile);
|
||||||
if (View) {
|
|
||||||
return new View(entry);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.onChanged = onChanged;
|
this.onChanged = onChanged;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +206,7 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
||||||
|
|
||||||
onUpdate(index: number, value: SimpleTile, param: any) {
|
onUpdate(index: number, value: SimpleTile, param: any) {
|
||||||
if (param === "shape") {
|
if (param === "shape") {
|
||||||
const ExpectedClass = viewClassForEntry(value);
|
const ExpectedClass = this.viewClassForTile(value);
|
||||||
const child = this.getChildInstanceByIndex(index);
|
const child = this.getChildInstanceByIndex(index);
|
||||||
if (!ExpectedClass || !(child instanceof ExpectedClass)) {
|
if (!ExpectedClass || !(child instanceof ExpectedClass)) {
|
||||||
// shape was updated, so we need to recreate the tile view,
|
// shape was updated, so we need to recreate the tile view,
|
||||||
|
|
|
@ -25,14 +25,10 @@ import {RedactedView} from "./timeline/RedactedView.js";
|
||||||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||||
import {GapView} from "./timeline/GapView.js";
|
import {GapView} from "./timeline/GapView.js";
|
||||||
import {CallTileView} from "./timeline/CallTileView";
|
import {CallTileView} from "./timeline/CallTileView";
|
||||||
|
import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView";
|
||||||
|
|
||||||
export type TileView = GapView | AnnouncementView | TextMessageView |
|
export function viewClassForTile(vm: SimpleTile): TileViewConstructor {
|
||||||
ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView;
|
switch (vm.shape) {
|
||||||
|
|
||||||
// TODO: this is what works for a ctor but doesn't actually check we constrain the returned ctors to the types above
|
|
||||||
type TileViewConstructor = (this: TileView, SimpleTile) => void;
|
|
||||||
export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
|
|
||||||
switch (entry.shape) {
|
|
||||||
case "gap":
|
case "gap":
|
||||||
return GapView;
|
return GapView;
|
||||||
case "announcement":
|
case "announcement":
|
||||||
|
@ -53,6 +49,8 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde
|
||||||
case "redacted":
|
case "redacted":
|
||||||
return RedactedView;
|
return RedactedView;
|
||||||
case "call":
|
case "call":
|
||||||
return CallTileView as any as TileViewConstructor;
|
return CallTileView;
|
||||||
|
default:
|
||||||
|
throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,11 @@ limitations under the License.
|
||||||
import {TemplateView} from "../../../general/TemplateView";
|
import {TemplateView} from "../../../general/TemplateView";
|
||||||
|
|
||||||
export class AnnouncementView extends TemplateView {
|
export class AnnouncementView extends TemplateView {
|
||||||
|
// ignore other arguments
|
||||||
|
constructor(vm) {
|
||||||
|
super(vm);
|
||||||
|
}
|
||||||
|
|
||||||
render(t) {
|
render(t) {
|
||||||
return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement));
|
return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,11 @@ import {Menu} from "../../../general/Menu.js";
|
||||||
import {ReactionsView} from "./ReactionsView.js";
|
import {ReactionsView} from "./ReactionsView.js";
|
||||||
|
|
||||||
export class BaseMessageView extends TemplateView {
|
export class BaseMessageView extends TemplateView {
|
||||||
constructor(value, renderFlags, tagName = "li") {
|
constructor(value, viewClassForTile, renderFlags, tagName = "li") {
|
||||||
super(value);
|
super(value);
|
||||||
this._menuPopup = null;
|
this._menuPopup = null;
|
||||||
this._tagName = tagName;
|
this._tagName = tagName;
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
// TODO An enum could be nice to make code easier to read at call sites.
|
// TODO An enum could be nice to make code easier to read at call sites.
|
||||||
this._renderFlags = renderFlags;
|
this._renderFlags = renderFlags;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,11 @@ import {TemplateView} from "../../../general/TemplateView";
|
||||||
import {spinner} from "../../../common.js";
|
import {spinner} from "../../../common.js";
|
||||||
|
|
||||||
export class GapView extends TemplateView {
|
export class GapView extends TemplateView {
|
||||||
|
// ignore other argument
|
||||||
|
constructor(vm) {
|
||||||
|
super(vm);
|
||||||
|
}
|
||||||
|
|
||||||
render(t) {
|
render(t) {
|
||||||
const className = {
|
const className = {
|
||||||
GapView: true,
|
GapView: true,
|
||||||
|
|
|
@ -16,15 +16,18 @@ limitations under the License.
|
||||||
|
|
||||||
import {renderStaticAvatar} from "../../../avatar";
|
import {renderStaticAvatar} from "../../../avatar";
|
||||||
import {TemplateView} from "../../../general/TemplateView";
|
import {TemplateView} from "../../../general/TemplateView";
|
||||||
import {viewClassForEntry} from "../common";
|
|
||||||
|
|
||||||
export class ReplyPreviewView extends TemplateView {
|
export class ReplyPreviewView extends TemplateView {
|
||||||
|
constructor(vm, viewClassForTile) {
|
||||||
|
super(vm);
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
|
}
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const viewClass = viewClassForEntry(vm);
|
const TileView = this._viewClassForTile(vm);
|
||||||
if (!viewClass) {
|
if (!TileView) {
|
||||||
throw new Error(`Shape ${vm.shape} is unrecognized.`)
|
throw new Error(`Shape ${vm.shape} is unrecognized.`)
|
||||||
}
|
}
|
||||||
const view = new viewClass(vm, { reply: true, interactive: false });
|
const view = new TileView(vm, this._viewClassForTile, { reply: true, interactive: false });
|
||||||
return t.div(
|
return t.div(
|
||||||
{ className: "ReplyPreviewView" },
|
{ className: "ReplyPreviewView" },
|
||||||
t.blockquote([
|
t.blockquote([
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class TextMessageView extends BaseMessageView {
|
||||||
return new ReplyPreviewError();
|
return new ReplyPreviewError();
|
||||||
}
|
}
|
||||||
else if (replyTile) {
|
else if (replyTile) {
|
||||||
return new ReplyPreviewView(replyTile);
|
return new ReplyPreviewView(replyTile, this._viewClassForTile);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
return null;
|
||||||
|
|
Loading…
Reference in a new issue