Merge branch 'master' into jitsi-audio-only
@ -1,6 +1,6 @@
|
|||||||
# Dimension Development
|
# Dimension Development
|
||||||
|
|
||||||
Dimension is split into two layers: the frontend (web) and backend. The frontend is responsible for interacting with the client (Riot) directly and hands off any complex work to the backend for processing.
|
Dimension is split into two layers: the frontend (web) and backend. The frontend is responsible for interacting with the client (Element) directly and hands off any complex work to the backend for processing.
|
||||||
|
|
||||||
**For help and support related to Dimension development, please visit:**
|
**For help and support related to Dimension development, please visit:**
|
||||||
[![#dimension:t2bot.io](https://img.shields.io/badge/matrix-%23dimension:t2bot.io-brightgreen.svg)](https://matrix.to/#/#dimension:t2bot.io)
|
[![#dimension:t2bot.io](https://img.shields.io/badge/matrix-%23dimension:t2bot.io-brightgreen.svg)](https://matrix.to/#/#dimension:t2bot.io)
|
||||||
@ -47,7 +47,7 @@ the bots can work well within their constraints.
|
|||||||
|
|
||||||
## Frontend Architecture
|
## Frontend Architecture
|
||||||
|
|
||||||
The frontend app is split into two major parts: The Riot frontend and the admin section. The components are nested under
|
The frontend app is split into two major parts: The Element frontend and the admin section. The components are nested under
|
||||||
their respective categories and route. For example, the edit page for the Jitsi widget is under the Widgets directory.
|
their respective categories and route. For example, the edit page for the Jitsi widget is under the Widgets directory.
|
||||||
|
|
||||||
The frontend is otherwise a fairly basic Angular 5 application: there's components, services, etc. The services should be
|
The frontend is otherwise a fairly basic Angular 5 application: there's components, services, etc. The services should be
|
||||||
|
41
Dockerfile
@ -1,47 +1,44 @@
|
|||||||
FROM node:10.16.0-alpine
|
FROM node:12.16.1-alpine AS builder
|
||||||
|
|
||||||
LABEL maintainer="Andreas Peters <support@aventer.biz>"
|
LABEL maintainer="Andreas Peters <support@aventer.biz>"
|
||||||
#Upstream URL: https://git.aventer.biz/AVENTER/docker-matrix-dimension
|
#Upstream URL: https://git.aventer.biz/AVENTER/docker-matrix-dimension
|
||||||
|
|
||||||
RUN apk add dos2unix --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/community/ --allow-untrusted
|
WORKDIR /home/node/matrix-dimension
|
||||||
|
|
||||||
RUN apk update && \
|
RUN mkdir -p /home/node/matrix-dimension
|
||||||
apk add --no-cache bash gcc python make g++ sqlite && \
|
|
||||||
mkdir /home/node/.npm-global && \
|
|
||||||
mkdir -p /home/node/app
|
|
||||||
|
|
||||||
COPY ./docker-entrypoint.sh /
|
|
||||||
COPY . /home/node/matrix-dimension
|
COPY . /home/node/matrix-dimension
|
||||||
|
|
||||||
|
RUN chown -R node /home/node/matrix-dimension
|
||||||
RUN chown -R node:node /home/node/app && \
|
|
||||||
chown -R node:node /home/node/.npm-global && \
|
|
||||||
chown -R node:node /home/node/matrix-dimension
|
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
ENV PATH=/home/node/.npm-global/bin:$PATH
|
RUN npm clean-install && \
|
||||||
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
|
node /home/node/matrix-dimension/scripts/convert-newlines.js /home/node/matrix-dimension/docker-entrypoint.sh && \
|
||||||
|
NODE_ENV=production npm run-script build
|
||||||
|
|
||||||
RUN cd /home/node/matrix-dimension && \
|
FROM node:12.16.1-alpine
|
||||||
npm install -D wd rimraf webpack webpack-command sqlite3 && \
|
|
||||||
NODE_ENV=production npm run-script build:web && npm run-script build:app
|
|
||||||
|
|
||||||
USER root
|
WORKDIR /home/node/matrix-dimension
|
||||||
|
|
||||||
RUN apk del gcc make g++ && \
|
COPY --from=builder /home/node/matrix-dimension/docker-entrypoint.sh /
|
||||||
rm /home/node/matrix-dimension/Dockerfile && \
|
|
||||||
rm /home/node/matrix-dimension/docker-entrypoint.sh && \
|
COPY --from=builder /home/node/matrix-dimension/build /home/node/matrix-dimension/build
|
||||||
dos2unix /docker-entrypoint.sh
|
COPY --from=builder /home/node/matrix-dimension/package* /home/node/matrix-dimension/
|
||||||
|
COPY --from=builder /home/node/matrix-dimension/config /home/node/matrix-dimension/config
|
||||||
|
|
||||||
|
RUN chown -R node /home/node/matrix-dimension
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
|
RUN npm clean-install --production
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
# Ensure the database doesn't get lost to the container
|
# Ensure the database doesn't get lost to the container
|
||||||
ENV DIMENSION_DB_PATH=/data/dimension.db
|
ENV DIMENSION_DB_PATH=/data/dimension.db
|
||||||
|
|
||||||
EXPOSE 8184
|
EXPOSE 8184
|
||||||
#CMD ["/bin/sh"]
|
# CMD ["/bin/sh"]
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
[![TravisCI badge](https://travis-ci.org/turt2live/matrix-dimension.svg?branch=master)](https://travis-ci.org/turt2live/matrix-dimension)
|
[![TravisCI badge](https://travis-ci.org/turt2live/matrix-dimension.svg?branch=master)](https://travis-ci.org/turt2live/matrix-dimension)
|
||||||
|
|
||||||
An open source integration manager for matrix clients, like Riot. For help and support, please visit
|
An open source integration manager for matrix clients, like Element. For help and support, please visit
|
||||||
us in [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io) on Matrix.
|
us in [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io) on Matrix.
|
||||||
|
|
||||||
# Installing Dimension / Running your own
|
# Installing Dimension / Running your own
|
||||||
@ -22,13 +22,13 @@ port. Dimension will use the first record it sees and will only communicate over
|
|||||||
3. **Verify the homeserver information in your configuration.** The name, access token, and client/
|
3. **Verify the homeserver information in your configuration.** The name, access token, and client/
|
||||||
server API URL all need to be set to point towards your homeserver. It may also be necessary to set the
|
server API URL all need to be set to point towards your homeserver. It may also be necessary to set the
|
||||||
federation URL if you're running a private server.
|
federation URL if you're running a private server.
|
||||||
4. **Run the troubleshooter.** If you're on Riot 1.1.0 or higher, type `/addwidget https://dimension.t2bot.io/widgets/manager-test`
|
4. **Run the troubleshooter.** If you're on Element, type `/addwidget https://dimension.t2bot.io/widgets/manager-test`
|
||||||
in a private room then click the button.
|
in a private room then click the button.
|
||||||
|
|
||||||
# Do I need an integrations manager?
|
# Do I need an integrations manager?
|
||||||
|
|
||||||
Integration managers aim to ease a user's interaction with the various services a homeserver may
|
Integration managers aim to ease a user's interaction with the various services a homeserver may
|
||||||
provide. Often times the integrations manager provided by Riot.im, named Modular, is more than suitable.
|
provide. Often times the integrations manager provided by Element, is more than suitable.
|
||||||
However, there are a few cases where running your own makes more sense:
|
However, there are a few cases where running your own makes more sense:
|
||||||
|
|
||||||
* Wanting to self-host all aspects of your services (client, homeserver, and integrations)
|
* Wanting to self-host all aspects of your services (client, homeserver, and integrations)
|
||||||
|
@ -30,7 +30,7 @@ homeserver:
|
|||||||
accessToken: "something"
|
accessToken: "something"
|
||||||
|
|
||||||
# These users can modify the integrations this Dimension supports.
|
# These users can modify the integrations this Dimension supports.
|
||||||
# To access the admin interface, open Dimension in Riot and click the settings icon.
|
# To access the admin interface, open Dimension in Element and click the settings icon.
|
||||||
admins:
|
admins:
|
||||||
- "@someone:domain.com"
|
- "@someone:domain.com"
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
set -e
|
|
||||||
|
|
||||||
cd /home/node/matrix-dimension/
|
|
||||||
|
|
||||||
if [ -f "/data/config.yaml" ]; then
|
if [ -f "/data/config.yaml" ]; then
|
||||||
cp /data/config.yaml /home/node/matrix-dimension/config/production.yaml
|
cp /data/config.yaml /home/node/matrix-dimension/config/production.yaml
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
## Installing Dimension
|
## Installing Dimension
|
||||||
|
|
||||||
**Note**: Dimension is currently only capable of running with Riot Web or Desktop. The iOS and Android
|
**Note**: Dimension is only supported in Element Web and Desktop at the moment. With some effort,
|
||||||
apps are not directly supported without compiling your own versions. In future, this should be handled
|
it can be used in other clients or Element iOS/Android, though is not guaranteed to work. In future,
|
||||||
by [an integration manager specification](https://github.com/turt2live/matrix-dimension/issues/262).
|
this should be handled by [an integration manager specification](https://github.com/turt2live/matrix-dimension/issues/262).
|
||||||
|
|
||||||
There are several options for installing Dimension. The easiest is dependent on how you have Riot
|
There are several options for installing Dimension. The easiest is dependent on how you have Element
|
||||||
and your homeserver set up. If you're using [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy),
|
and your homeserver set up. If you're using [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy),
|
||||||
there are already options for configuring Dimension.
|
there are already options for configuring Dimension.
|
||||||
|
|
||||||
### Step 0: Requirements
|
### Step 0: Requirements
|
||||||
|
|
||||||
You will need a functioning homeserver (such as [Synapse](https://github.com/matrix-org/synapse)) and
|
You will need a functioning homeserver (such as [Synapse](https://github.com/matrix-org/synapse)) and
|
||||||
a client to access Dimension with. Currently, that means using [Riot Web or Desktop](https://riot.im).
|
a client to access Dimension with. Currently, that means using [Element Web or Desktop](https://element.io).
|
||||||
|
|
||||||
Additionally, you will need to be able to host Dimension on a dedicated domain. If your homeserver
|
Additionally, you will need to be able to host Dimension on a dedicated domain. If your homeserver
|
||||||
is set up for example.org, we recommend using dimension.example.org for Dimension.
|
is set up for example.org, we recommend using dimension.example.org for Dimension.
|
||||||
|
|
||||||
Finally, this guide assumes you are running nginx as a webserver for Riot, your homeserver, and
|
Finally, this guide assumes you are running nginx as a webserver for Element, your homeserver, and
|
||||||
Dimension. A basic configuration before setting up Dimension would be:
|
Dimension. A basic configuration before setting up Dimension would be:
|
||||||
|
|
||||||
```conf
|
```conf
|
||||||
@ -34,7 +34,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
# Simple configuration for serving Riot
|
# Simple configuration for serving Element
|
||||||
server_name chat.example.org;
|
server_name chat.example.org;
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
@ -62,7 +62,7 @@ it yourself.
|
|||||||
If you're using Docker, create a directory at `/etc/dimension` (or wherever you'd like - just remember
|
If you're using Docker, create a directory at `/etc/dimension` (or wherever you'd like - just remember
|
||||||
where it is!).
|
where it is!).
|
||||||
|
|
||||||
To build Dimension yourself, you'll need Node 10, npm 6, and 2-4gb of RAM. The following steps are enough
|
To build Dimension yourself, you'll need Node 10+, npm 6+, and 2-4gb of RAM. The following steps are enough
|
||||||
to get you started:
|
to get you started:
|
||||||
```bash
|
```bash
|
||||||
# Download dimension
|
# Download dimension
|
||||||
@ -134,11 +134,11 @@ Reload or restart nginx after creating the configuration.
|
|||||||
### Step 5: Final steps
|
### Step 5: Final steps
|
||||||
|
|
||||||
If everything went according to plan, you should be able to visit `https://dimension.example.org`
|
If everything went according to plan, you should be able to visit `https://dimension.example.org`
|
||||||
and see instructions for configuring Riot. If you don't, your configuration isn't working as
|
and see instructions for configuring Element. If you don't, your configuration isn't working as
|
||||||
intended - double check that all the configuration is set up and visit [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io)
|
intended - double check that all the configuration is set up and visit [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io)
|
||||||
for further help.
|
for further help.
|
||||||
|
|
||||||
After configuring Riot, click the integrations button (4 squares in the top right of any room) and
|
After configuring Element, click the integrations button (4 squares in the top right of any room) and
|
||||||
then click the gear icon. If you don't see a gear icon, you're not an admin in the config. This is
|
then click the gear icon. If you don't see a gear icon, you're not an admin in the config. This is
|
||||||
where you'll configure different integrations as Dimension doesn't ship with anything enabled by
|
where you'll configure different integrations as Dimension doesn't ship with anything enabled by
|
||||||
default - click around and start enabling things.
|
default - click around and start enabling things.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Riot Widgets
|
# Element Widgets
|
||||||
|
|
||||||
Riot uses some special interaction with the integration manager to make for a clean user experience.
|
Element uses some special interaction with the integration manager to make for a clean user experience.
|
||||||
|
|
||||||
|
|
||||||
### Edit Widget button
|
### Edit Widget button
|
@ -1,12 +1,12 @@
|
|||||||
# Riot's Widget API
|
# Element's Widget API
|
||||||
|
|
||||||
Widgets and Riot communicate using cross-origin messages in a defined format (described in this document). Widgets have access to the entire Scalar Client API, but generally do not need any of the endpoints there. Riot provides additional APIs available to particular widgets for which the integrations manager can not access. The full source for the widget messaging layer in Riot can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/WidgetMessaging.js). The API is restricted to ensure rogue widgets cannot take over the Riot instance.
|
Widgets and Element communicate using cross-origin messages in a defined format (described in this document). Widgets have access to the entire Scalar Client API, but generally do not need any of the endpoints there. Element provides additional APIs available to particular widgets for which the integrations manager can not access. The full source for the widget messaging layer in Element can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/WidgetMessaging.js). The API is restricted to ensure rogue widgets cannot take over the Element instance.
|
||||||
|
|
||||||
**Note**: This is largely out of date and better documented in the Matrix spec nowadays. See https://github.com/matrix-org/matrix-doc/issues/1236 for more information.
|
**Note**: This is largely out of date and better documented in the Matrix spec nowadays. See https://github.com/matrix-org/matrix-doc/issues/1236 for more information.
|
||||||
|
|
||||||
## Setting up communications
|
## Setting up communications
|
||||||
|
|
||||||
Riot will automatically open a channel for receiving messages. The widget needs to do the same so it can speak to Riot. Here's some sample JavaScript that will do this for us:
|
Element will automatically open a channel for receiving messages. The widget needs to do the same so it can speak to Element. Here's some sample JavaScript that will do this for us:
|
||||||
|
|
||||||
```
|
```
|
||||||
window.addEventListener("message", function(event) {
|
window.addEventListener("message", function(event) {
|
@ -1,13 +1,13 @@
|
|||||||
# Scalar Authentication / Registration
|
# Scalar Authentication / Registration
|
||||||
|
|
||||||
When the "Manage Integrations" button is first clicked by a user, Riot will try and register with the Integrations Manager
|
When the "Manage Integrations" button is first clicked by a user, Element will try and register with the Integrations Manager
|
||||||
to get a `scalar_token` that it then uses to authenticate all future requests with the manager.
|
to get a `scalar_token` that it then uses to authenticate all future requests with the manager.
|
||||||
|
|
||||||
## `$restUrl/register`
|
## `$restUrl/register`
|
||||||
|
|
||||||
This ends up mapping to `/api/v1/scalar/register` when Dimension is correctly set up for a Riot instance.
|
This ends up mapping to `/api/v1/scalar/register` when Dimension is correctly set up for a Element instance.
|
||||||
|
|
||||||
Riot will POST to this endpoint an OpenID object that looks similar to the following:
|
Element will POST to this endpoint an OpenID object that looks similar to the following:
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"access_token": "ABCDEFGH",
|
"access_token": "ABCDEFGH",
|
||||||
@ -39,4 +39,4 @@ following JSON is more than enough:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Riot will now use this token in future requests by hitting the `"integrations_ui_url"` with `?access_token=some_generated_string`.
|
Element will now use this token in future requests by hitting the `"integrations_ui_url"` with `?access_token=some_generated_string`.
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# Scalar API (Riot)
|
# Scalar API (Element)
|
||||||
|
|
||||||
Scalar and Riot communicate using cross-origin messages in a defined format (described in this document). The full source for the messaging layer in Riot can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/ScalarMessaging.js). With this API, the integrations manager is able to invite users, get some basic state information, and interact with the room in a limited capacity. The API is intentionally restricted to ensure that misbehaving domains don't have full control over Riot.
|
Scalar and Element communicate using cross-origin messages in a defined format (described in this document). The full source for the messaging layer in Element can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/ScalarMessaging.js). With this API, the integrations manager is able to invite users, get some basic state information, and interact with the room in a limited capacity. The API is intentionally restricted to ensure that misbehaving domains don't have full control over Element.
|
||||||
|
|
||||||
## Setting up communications
|
## Setting up communications
|
||||||
|
|
||||||
Riot will automatically open a channel for receiving messages. The integrations manager needs to do the same so it can speak to Riot. Here's some sample JavaScript that will do this for us:
|
Element will automatically open a channel for receiving messages. The integrations manager needs to do the same so it can speak to Element. Here's some sample JavaScript that will do this for us:
|
||||||
|
|
||||||
```
|
```
|
||||||
window.addEventListener("message", function(event) {
|
window.addEventListener("message", function(event) {
|
||||||
@ -393,7 +393,7 @@ sendMessage("set_widget", "!curbf:matrix.org", null, {
|
|||||||
```
|
```
|
||||||
|
|
||||||
*Note*: Widgets are documented by the matrix.org team [on this Google Doc](https://docs.google.com/document/d/1TiWNDcEOULeRYQpkJHQDjgIW32ohIJSi5MKv9oRdzCo/edit). That document is the source of truth for the event structure and usage.
|
*Note*: Widgets are documented by the matrix.org team [on this Google Doc](https://docs.google.com/document/d/1TiWNDcEOULeRYQpkJHQDjgIW32ohIJSi5MKv9oRdzCo/edit). That document is the source of truth for the event structure and usage.
|
||||||
*Note*: `scalar_token` will be appended to the query string if the widget's url matches the API URL of the integration manager (in Riot)
|
*Note*: `scalar_token` will be appended to the query string if the widget's url matches the API URL of the integration manager (in Element)
|
||||||
|
|
||||||
### Getting the room's encryption status
|
### Getting the room's encryption status
|
||||||
|
|
||||||
|
@ -709,25 +709,9 @@ None of these are officially documented, and are subject to change.
|
|||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
"session": {
|
"session": {
|
||||||
"Repos": [
|
"Repos": [
|
||||||
{
|
|
||||||
"name": "riot-welcome-page",
|
|
||||||
"description": "A welcome page specific for tang.ents.ca (built for Riot)",
|
|
||||||
"private": false,
|
|
||||||
"html_url": "https:\/\/github.com\/ENTS-Source\/riot-welcome-page",
|
|
||||||
"created_at": "2017-06-10T16:54:37Z",
|
|
||||||
"updated_at": "2017-06-10T19:10:21Z",
|
|
||||||
"pushed_at": "2017-06-10T18:15:07Z",
|
|
||||||
"fork": false,
|
|
||||||
"full_name": "ENTS-Source\/riot-welcome-page",
|
|
||||||
"permissions": {
|
|
||||||
"admin": true,
|
|
||||||
"pull": true,
|
|
||||||
"push": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "matrix-dimension",
|
"name": "matrix-dimension",
|
||||||
"description": "An alternative integrations manager for Riot",
|
"description": "An alternative integrations manager for Element",
|
||||||
"private": false,
|
"private": false,
|
||||||
"html_url": "https:\/\/github.com\/turt2live\/matrix-dimension",
|
"html_url": "https:\/\/github.com\/turt2live\/matrix-dimension",
|
||||||
"created_at": "2017-05-25T21:41:55Z",
|
"created_at": "2017-05-25T21:41:55Z",
|
||||||
|
752
package-lock.json
generated
12
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-dimension",
|
"name": "matrix-dimension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "An alternative integrations manager for Riot",
|
"description": "An alternative integrations manager for Element",
|
||||||
"main": "build/app/index.js",
|
"main": "build/app/index.js",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"git-rev-sync": "^1.12.0",
|
"git-rev-sync": "^1.12.0",
|
||||||
"isipaddress": "0.0.2",
|
"isipaddress": "0.0.2",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.19",
|
||||||
"matrix-bot-sdk": "^0.3.8",
|
"matrix-bot-sdk": "^0.3.8",
|
||||||
"matrix-js-snippets": "^0.2.8",
|
"matrix-js-snippets": "^0.2.8",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
@ -44,9 +44,9 @@
|
|||||||
"request-promise": "^4.2.4",
|
"request-promise": "^4.2.4",
|
||||||
"require-dir-all": "^0.4.15",
|
"require-dir-all": "^0.4.15",
|
||||||
"semver": "^6.0.0",
|
"semver": "^6.0.0",
|
||||||
"sequelize": "^5.15.1",
|
"sequelize": "^5.18.4",
|
||||||
"sequelize-typescript": "^1.0.0",
|
"sequelize-typescript": "^1.0.0",
|
||||||
"sharp": "^0.21.1",
|
"sharp": "^0.25.3",
|
||||||
"split-host": "^0.1.1",
|
"split-host": "^0.1.1",
|
||||||
"spotify-uri": "^1.0.0",
|
"spotify-uri": "^1.0.0",
|
||||||
"sqlite3": "^4.0.9",
|
"sqlite3": "^4.0.9",
|
||||||
@ -95,12 +95,12 @@
|
|||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"iso-639-1": "^2.0.5",
|
"iso-639-1": "^2.0.5",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.5.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"mini-css-extract-plugin": "^0.7.0",
|
"mini-css-extract-plugin": "^0.7.0",
|
||||||
"ng2-breadcrumbs": "^0.1.281",
|
"ng2-breadcrumbs": "^0.1.281",
|
||||||
"ngx-modialog": "^5.0.1",
|
"ngx-modialog": "^5.0.1",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.14.1",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^12.0.1",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
|
23
scripts/convert-newlines.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
if (process.argv.length !== 3) {
|
||||||
|
console.error('Wrong number of arguments');
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = process.argv.pop();
|
||||||
|
|
||||||
|
const fileExists = await util.promisify(fs.exists)(filePath);
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
const file = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
filePath,
|
||||||
|
file
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/\r/, '\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
@ -40,7 +40,7 @@ export default class Webserver {
|
|||||||
|
|
||||||
// We register the default route last to make sure we don't override anything by accident.
|
// We register the default route last to make sure we don't override anything by accident.
|
||||||
// We'll pass off all other requests to the web app
|
// We'll pass off all other requests to the web app
|
||||||
this.app.get(/(widgets\/|riot\/|\/)*/, (_req, res) => {
|
this.app.get(/(widgets\/|riot\/|element\/|\/)*/, (_req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "..", "..", "web", "index.html"));
|
res.sendFile(path.join(__dirname, "..", "..", "web", "index.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
import { Context, GET, Path, PathParam, POST, DELETE, Security, ServiceContext } from "typescript-rest";
|
||||||
import StickerPack from "../../db/models/StickerPack";
|
import StickerPack from "../../db/models/StickerPack";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
|
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
|
||||||
@ -49,6 +49,19 @@ export class AdminStickerService {
|
|||||||
return {}; // 200 OK
|
return {}; // 200 OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("packs/:id")
|
||||||
|
@Security([ROLE_ADMIN])
|
||||||
|
public async removePack(@PathParam("id") packId: number): Promise<any> {
|
||||||
|
const pack = await StickerPack.findByPk(packId);
|
||||||
|
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
||||||
|
|
||||||
|
await pack.destroy();
|
||||||
|
Cache.for(CACHE_STICKERS).clear();
|
||||||
|
|
||||||
|
return {}; // 200 OK
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("packs/import/telegram")
|
@Path("packs/import/telegram")
|
||||||
@Security([ROLE_USER, ROLE_ADMIN])
|
@Security([ROLE_USER, ROLE_ADMIN])
|
||||||
@ -85,16 +98,18 @@ export class AdminStickerService {
|
|||||||
for (const tgSticker of tgPack.stickers) {
|
for (const tgSticker of tgPack.stickers) {
|
||||||
LogService.info("AdminStickerService", "Importing sticker from " + tgSticker.url);
|
LogService.info("AdminStickerService", "Importing sticker from " + tgSticker.url);
|
||||||
const buffer = await mx.downloadFromUrl(tgSticker.url);
|
const buffer = await mx.downloadFromUrl(tgSticker.url);
|
||||||
const png = await sharp(buffer).resize({
|
const image = await sharp(buffer);
|
||||||
width: 512,
|
const metadata = await image.metadata();
|
||||||
height: 512,
|
const png = await image.resize({
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
fit: 'contain',
|
fit: 'contain',
|
||||||
background: 'rgba(0,0,0,0)',
|
background: 'rgba(0,0,0,0)',
|
||||||
}).png().toBuffer();
|
}).png().toBuffer();
|
||||||
const mxc = await mx.upload(png, "image/png");
|
const mxc = await mx.upload(png, "image/png");
|
||||||
const serverName = mxc.substring("mxc://".length).split("/")[0];
|
const serverName = mxc.substring("mxc://".length).split("/")[0];
|
||||||
const contentId = mxc.substring("mxc://".length).split("/")[1];
|
const contentId = mxc.substring("mxc://".length).split("/")[1];
|
||||||
const thumbMxc = await mx.uploadFromUrl(await mx.getThumbnailUrl(serverName, contentId, 512, 512, "scale", false), "image/png");
|
const thumbMxc = await mx.uploadFromUrl(await mx.getThumbnailUrl(serverName, contentId, metadata.width, metadata.height, "scale", false), "image/png");
|
||||||
|
|
||||||
stickers.push(await Sticker.create({
|
stickers.push(await Sticker.create({
|
||||||
packId: pack.id,
|
packId: pack.id,
|
||||||
@ -102,8 +117,8 @@ export class AdminStickerService {
|
|||||||
description: tgSticker.emoji,
|
description: tgSticker.emoji,
|
||||||
imageMxc: mxc,
|
imageMxc: mxc,
|
||||||
thumbnailMxc: thumbMxc,
|
thumbnailMxc: thumbMxc,
|
||||||
thumbnailWidth: 512,
|
thumbnailWidth: metadata.width,
|
||||||
thumbnailHeight: 512,
|
thumbnailHeight: metadata.height,
|
||||||
mimetype: "image/png",
|
mimetype: "image/png",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
207
src/api/dimension/DimensionBigBlueButtonService.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { GET, Path, QueryParam } from "typescript-rest";
|
||||||
|
import * as request from "request";
|
||||||
|
import { LogService } from "matrix-js-snippets";
|
||||||
|
import { URL } from "url";
|
||||||
|
import { BigBlueButtonJoinRequest } from "../../models/Widget";
|
||||||
|
import { BigBlueButtonJoinResponse } from "../../models/WidgetResponses";
|
||||||
|
import { AutoWired } from "typescript-ioc/es6";
|
||||||
|
import { ApiError } from "../ApiError";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for the BigBlueButton widget.
|
||||||
|
*/
|
||||||
|
@Path("/api/v1/dimension/bigbluebutton")
|
||||||
|
@AutoWired
|
||||||
|
export class DimensionBigBlueButtonService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A regex used for extracting the authenticity token from the HTML of a
|
||||||
|
* greenlight server response
|
||||||
|
*/
|
||||||
|
private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`);
|
||||||
|
|
||||||
|
// join handles the request from a client to join a BigBlueButton meeting
|
||||||
|
//
|
||||||
|
// The client is expected to send a link created by greenlight, the nice UI
|
||||||
|
// that's recommended to be installed on top of BBB, which is itself a BBB
|
||||||
|
// client.
|
||||||
|
//
|
||||||
|
// This greenlight link is nice, but greenlight unfortunately doesn't have any
|
||||||
|
// API, and no simple way for us to translate a link from it into a BBB meeting
|
||||||
|
// URL. It's intended to be loaded by browsers. You enter your preferred name,
|
||||||
|
// click submit, you potentially wait for the meeting to start, and then you
|
||||||
|
// finally get the link to join the meeting, and you load that.
|
||||||
|
//
|
||||||
|
// As there's no other way to do it, we just reverse-engineer it and pretend
|
||||||
|
// to be a browser below. We can't do this from the client side as widgets
|
||||||
|
// run in iframes and browsers can't inspect the content of an iframe if
|
||||||
|
// it's running on a separate domain.
|
||||||
|
//
|
||||||
|
// So the client gets a greenlight URL pasted into it. The flow is then:
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// +---------+ +-----------+ +-------------+ +-----+
|
||||||
|
// | Client | | Dimension | | Greenlight | | BBB |
|
||||||
|
// +---------+ +-----------+ +-------------+ +-----+
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | /bigbluebutton/join&greenlightUrl=https://.../abc-def-123&fullName=bob | | |
|
||||||
|
// |---------------------------------------------------------------------------->| | |
|
||||||
|
// | | | |
|
||||||
|
// | | GET https://.../abc-def-123 | |
|
||||||
|
// | |-------------------------------------------------------------------------------------->| |
|
||||||
|
// | | | |
|
||||||
|
// | | Have some HTML | |
|
||||||
|
// | |<--------------------------------------------------------------------------------------| |
|
||||||
|
// | | | |
|
||||||
|
// | | Extract authenticity_token from HTML | |
|
||||||
|
// | |------------------------------------- | |
|
||||||
|
// | | | | |
|
||||||
|
// | |<------------------------------------ | |
|
||||||
|
// | | | |
|
||||||
|
// | | Extract cookies from HTTP response | |
|
||||||
|
// | |----------------------------------- | |
|
||||||
|
// | | | | |
|
||||||
|
// | |<---------------------------------- | |
|
||||||
|
// | | | |
|
||||||
|
// | | POST https://.../abc-def-123&authenticity_token=...&abc-def-123[join_name]=bob | |
|
||||||
|
// | |-------------------------------------------------------------------------------------->| |
|
||||||
|
// |===============================================================================================If the meeting has not started yet================================================|
|
||||||
|
// | | | |
|
||||||
|
// | | HTML https://.../abc-def-123 Meeting not started | |
|
||||||
|
// | |<--------------------------------------------------------------------------------------| |
|
||||||
|
// | | | |
|
||||||
|
// | 400 MEETING_NOT_STARTED_YET | | |
|
||||||
|
// |<----------------------------------------------------------------------------| | |
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | Wait a bit and restart the process | | |
|
||||||
|
// |------------------------------------- | | |
|
||||||
|
// | | | | |
|
||||||
|
// |<------------------------------------ | | |
|
||||||
|
// | | | |
|
||||||
|
// |=================================================================================================================================================================================|
|
||||||
|
// | | | |
|
||||||
|
// | | 302 Location: https://bbb.example.com/join?... | |
|
||||||
|
// | |<--------------------------------------------------------------------------------------| |
|
||||||
|
// | | | |
|
||||||
|
// | | Extract value of Location header | |
|
||||||
|
// | |--------------------------------- | |
|
||||||
|
// | | | | |
|
||||||
|
// | |<-------------------------------- | |
|
||||||
|
// | | | |
|
||||||
|
// | https://bbb.example.com/join?... | | |
|
||||||
|
// |<----------------------------------------------------------------------------| | |
|
||||||
|
// | | | |
|
||||||
|
// | GET https://bbb.example.com/join?... | | |
|
||||||
|
// |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->|
|
||||||
|
// | | | |
|
||||||
|
// | | Send back meeting page HTML | |
|
||||||
|
// |<--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
//
|
||||||
|
@GET
|
||||||
|
@Path("join")
|
||||||
|
public async join(
|
||||||
|
joinRequest: BigBlueButtonJoinRequest,
|
||||||
|
@QueryParam("greenlightUrl") greenlightURL: string,
|
||||||
|
@QueryParam("fullName") fullName: string,
|
||||||
|
): Promise<BigBlueButtonJoinResponse|ApiError> {
|
||||||
|
// Parse the greenlight url and retrieve the path
|
||||||
|
const greenlightMeetingID = new URL(greenlightURL).pathname;
|
||||||
|
|
||||||
|
LogService.info("BigBlueButton", "URL from client: " + greenlightURL);
|
||||||
|
LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID);
|
||||||
|
LogService.info("BigBlueButton", "Name given from client: " + fullName);
|
||||||
|
LogService.info("BigBlueButton", joinRequest);
|
||||||
|
|
||||||
|
// Query the URL the user has given us
|
||||||
|
let response = await this.doRequest("GET", greenlightURL);
|
||||||
|
if (!response || !response.body) {
|
||||||
|
throw new Error("Invalid response from Greenlight server while joining meeting");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to extract the authenticity token
|
||||||
|
const matches = response.body.match(this.authenticityTokenRegexp);
|
||||||
|
if (matches.length < 2) {
|
||||||
|
throw new Error("Unable to find authenticity token for given 'greenlightUrl' parameter");
|
||||||
|
}
|
||||||
|
const authenticityToken = matches[1];
|
||||||
|
|
||||||
|
// Give the authenticity token and desired name to greenlight, getting the
|
||||||
|
// join URL in return. Greenlight will send the URL back as a Location:
|
||||||
|
// header. We want to extract and return the contents of this header, rather
|
||||||
|
// than following it ourselves
|
||||||
|
|
||||||
|
// Add authenticity token and full name to the query parameters
|
||||||
|
let queryParams = {authenticity_token: authenticityToken};
|
||||||
|
queryParams[`${greenlightMeetingID}[join_name]`] = fullName;
|
||||||
|
|
||||||
|
// Request the updated URL
|
||||||
|
response = await this.doRequest("POST", greenlightURL, queryParams, "{}", false);
|
||||||
|
if (!response || !response.body) {
|
||||||
|
throw new Error("Invalid response from Greenlight server while joining meeting");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("location" in response.response.headers)) {
|
||||||
|
// We didn't get a meeting URL back. This could either happen due to an issue with the parameters
|
||||||
|
// sent to the server... or the meeting simply hasn't started yet.
|
||||||
|
|
||||||
|
// Assume it hasn't started yet. Send a custom error code back to the client informing them to try
|
||||||
|
// again in a bit
|
||||||
|
return new ApiError(
|
||||||
|
400,
|
||||||
|
{error: "Unable to find meeting URL in greenlight response"},
|
||||||
|
"WAITING_FOR_MEETING_START",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the join URL for the client to load
|
||||||
|
const joinUrl = response.response.headers["location"];
|
||||||
|
LogService.info("BigBlueButton", "Sending back join URL: " + joinUrl)
|
||||||
|
return {url: joinUrl};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doRequest(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
qs?: any,
|
||||||
|
body?: any,
|
||||||
|
followRedirect: boolean = true,
|
||||||
|
): Promise<any> {
|
||||||
|
// Query a URL, expecting an HTML response in return
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request({
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
qs: qs,
|
||||||
|
body: body,
|
||||||
|
followRedirect: followRedirect,
|
||||||
|
jar: true, // remember cookies between requests
|
||||||
|
json: false, // expect html
|
||||||
|
}, (err, res, _body) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
LogService.error("BigBlueButtonWidget", "Error calling " + url);
|
||||||
|
LogService.error("BigBlueButtonWidget", err);
|
||||||
|
reject(err);
|
||||||
|
} else if (!res) {
|
||||||
|
LogService.error("BigBlueButtonWidget", "There is no response for " + url);
|
||||||
|
reject(new Error("No response provided - is the service online?"));
|
||||||
|
} else if (res.statusCode !== 200 && res.statusCode !== 302) {
|
||||||
|
LogService.error("BigBlueButtonWidget", "Got status code " + res.statusCode + " when calling " + url);
|
||||||
|
LogService.error("BigBlueButtonWidget", res.body);
|
||||||
|
reject({body: res.body, status: res.statusCode});
|
||||||
|
} else {
|
||||||
|
resolve({body: res.body, response: res});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
LogService.error("BigBlueButtonWidget", e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,7 +24,7 @@ export class MatrixWellknownService {
|
|||||||
public async getIntegrations(): Promise<WellknownResponse> {
|
public async getIntegrations(): Promise<WellknownResponse> {
|
||||||
const parsed = new URL(config.dimension.publicUrl);
|
const parsed = new URL(config.dimension.publicUrl);
|
||||||
|
|
||||||
parsed.pathname = '/riot';
|
parsed.pathname = '/element';
|
||||||
const uiUrl = parsed.toString();
|
const uiUrl = parsed.toString();
|
||||||
|
|
||||||
parsed.pathname = '/api/v1/scalar';
|
parsed.pathname = '/api/v1/scalar';
|
||||||
|
@ -11,7 +11,7 @@ interface UrlPreviewResponse {
|
|||||||
page_title_cache_item: {
|
page_title_cache_item: {
|
||||||
expires: string; // "2017-12-18T04:20:04.001806738Z"
|
expires: string; // "2017-12-18T04:20:04.001806738Z"
|
||||||
cached_response_err: string;
|
cached_response_err: string;
|
||||||
cached_title: string; // the actual thing riot uses
|
cached_title: string; // the actual thing Element uses
|
||||||
};
|
};
|
||||||
error: {
|
error: {
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -17,6 +17,7 @@ export interface DimensionConfig {
|
|||||||
database: {
|
database: {
|
||||||
file: string;
|
file: string;
|
||||||
botData: string;
|
botData: string;
|
||||||
|
uri: string;
|
||||||
};
|
};
|
||||||
admins: string[];
|
admins: string[];
|
||||||
goneb: {
|
goneb: {
|
||||||
|
@ -35,6 +35,11 @@ class _DimensionStore {
|
|||||||
private sequelize: Sequelize;
|
private sequelize: Sequelize;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
if (process.env.DATABASE_URI || config.database.uri ) {
|
||||||
|
this.sequelize = new Sequelize(process.env.DATABASE_URI || config.database.uri , {
|
||||||
|
logging: i => LogService.verbose("DimensionStore [SQL]", i)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sequelize = new Sequelize({
|
this.sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
database: "dimension",
|
database: "dimension",
|
||||||
@ -43,6 +48,7 @@ class _DimensionStore {
|
|||||||
password: "",
|
password: "",
|
||||||
logging: i => LogService.verbose("DimensionStore [SQL]", i)
|
logging: i => LogService.verbose("DimensionStore [SQL]", i)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this.sequelize.addModels([
|
this.sequelize.addModels([
|
||||||
User,
|
User,
|
||||||
UserScalarToken,
|
UserScalarToken,
|
||||||
|
@ -18,7 +18,9 @@ export default {
|
|||||||
licensePath: "/licenses/cc_by-nc_4.0.txt",
|
licensePath: "/licenses/cc_by-nc_4.0.txt",
|
||||||
}
|
}
|
||||||
]))
|
]))
|
||||||
.then(packId => {
|
.then(() => queryInterface.rawSelect('dimension_sticker_packs', { where: { name: "Loading Artist" } }, ['id']))
|
||||||
|
.then(packIds => {
|
||||||
|
const packId = Array.isArray(packIds) ? packIds[0] : packIds;
|
||||||
return queryInterface.bulkInsert("dimension_stickers", [
|
return queryInterface.bulkInsert("dimension_stickers", [
|
||||||
{
|
{
|
||||||
packId: packId,
|
packId: packId,
|
||||||
|
23
src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { QueryInterface } from "sequelize";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkInsert("dimension_widgets", [
|
||||||
|
{
|
||||||
|
type: "bigbluebutton",
|
||||||
|
name: "BigBlueButton",
|
||||||
|
avatarUrl: "/img/avatars/bigbluebutton.png",
|
||||||
|
isEnabled: true,
|
||||||
|
isPublic: true,
|
||||||
|
description: "Embed a BigBlueButton conference",
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
},
|
||||||
|
down: (queryInterface: QueryInterface) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => queryInterface.bulkDelete("dimension_widgets", {
|
||||||
|
type: "bigbluebutton",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
7
src/models/Widget.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface BigBlueButtonJoinRequest {
|
||||||
|
// A URL supplied by greenlight, BigBlueButton's nice UI project that is itself
|
||||||
|
// a BigBlueButton client
|
||||||
|
greenlightUrl: string;
|
||||||
|
// The name the user wishes to join the meeting with
|
||||||
|
fullName: string;
|
||||||
|
}
|
4
src/models/WidgetResponses.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface BigBlueButtonJoinResponse {
|
||||||
|
// The meeting URL the client should load to join the meeting
|
||||||
|
url: string;
|
||||||
|
}
|
@ -52,6 +52,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<ui-switch [checked]="pack.isEnabled" size="small" [disabled]="isUpdating"
|
<ui-switch [checked]="pack.isEnabled" size="small" [disabled]="isUpdating"
|
||||||
(change)="toggleEnabled(pack)"></ui-switch>
|
(change)="toggleEnabled(pack)"></ui-switch>
|
||||||
|
<span *ngIf="!pack.isEnabled && !isUpdating" class="removeButton" title="remove stickerpack" (click)="removePack(pack)">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -7,6 +7,11 @@ tr td:last-child {
|
|||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.removeButton {
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.telegram-import {
|
.telegram-import {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
@ -68,4 +68,22 @@ export class AdminStickerPacksComponent implements OnInit {
|
|||||||
this.toaster.pop("error", "Error importing sticker pack");
|
this.toaster.pop("error", "Error importing sticker pack");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removePack(pack: FE_StickerPack) {
|
||||||
|
this.isUpdating = true;
|
||||||
|
this.adminStickers.removePack(pack.id).then(() => {
|
||||||
|
for (let i = 0; i < this.packs.length; ++i) {
|
||||||
|
if (this.packs[i].id === pack.id) {
|
||||||
|
this.packs.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.isUpdating = false;
|
||||||
|
this.toaster.pop("success", "Sticker pack removed");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.isUpdating = false;
|
||||||
|
this.toaster.pop("error", "Error removing sticker pack");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,9 @@ import { CKEditorModule } from "@ckeditor/ckeditor5-angular";
|
|||||||
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
|
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
|
||||||
import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component";
|
import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component";
|
||||||
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
|
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
|
||||||
|
import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component";
|
||||||
|
import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component";
|
||||||
|
import { BigBlueButtonApiService } from "./shared/services/integrations/bigbluebutton-api.service";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -147,7 +150,9 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
|
|||||||
FullscreenButtonComponent,
|
FullscreenButtonComponent,
|
||||||
VideoWidgetWrapperComponent,
|
VideoWidgetWrapperComponent,
|
||||||
JitsiWidgetWrapperComponent,
|
JitsiWidgetWrapperComponent,
|
||||||
|
BigBlueButtonWidgetWrapperComponent,
|
||||||
GCalWidgetWrapperComponent,
|
GCalWidgetWrapperComponent,
|
||||||
|
BigBlueButtonConfigComponent,
|
||||||
RiotHomeComponent,
|
RiotHomeComponent,
|
||||||
IboxComponent,
|
IboxComponent,
|
||||||
ConfigScreenWidgetComponent,
|
ConfigScreenWidgetComponent,
|
||||||
@ -234,6 +239,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
|
|||||||
AdminStickersApiService,
|
AdminStickersApiService,
|
||||||
MediaService,
|
MediaService,
|
||||||
StickerApiService,
|
StickerApiService,
|
||||||
|
BigBlueButtonApiService,
|
||||||
AdminTelegramApiService,
|
AdminTelegramApiService,
|
||||||
TelegramApiService,
|
TelegramApiService,
|
||||||
AdminWebhooksApiService,
|
AdminWebhooksApiService,
|
||||||
|
@ -2,6 +2,8 @@ import { RouterModule, Routes } from "@angular/router";
|
|||||||
import { HomeComponent } from "./home/home.component";
|
import { HomeComponent } from "./home/home.component";
|
||||||
import { RiotComponent } from "./riot/riot.component";
|
import { RiotComponent } from "./riot/riot.component";
|
||||||
import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component";
|
import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component";
|
||||||
|
import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component";
|
||||||
|
import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component";
|
||||||
import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component";
|
import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component";
|
||||||
import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component";
|
import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component";
|
||||||
import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component";
|
import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component";
|
||||||
@ -51,6 +53,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
|
|||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "", component: HomeComponent},
|
{path: "", component: HomeComponent},
|
||||||
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
|
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
|
||||||
|
{path: "element", pathMatch: "full", redirectTo: "riot-app"},
|
||||||
{
|
{
|
||||||
path: "riot-app",
|
path: "riot-app",
|
||||||
component: RiotComponent,
|
component: RiotComponent,
|
||||||
@ -179,6 +182,11 @@ const routes: Routes = [
|
|||||||
component: CustomWidgetConfigComponent,
|
component: CustomWidgetConfigComponent,
|
||||||
data: {breadcrumb: "Custom Widgets", name: "Custom Widgets"},
|
data: {breadcrumb: "Custom Widgets", name: "Custom Widgets"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "bigbluebutton",
|
||||||
|
component: BigBlueButtonConfigComponent,
|
||||||
|
data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "etherpad",
|
path: "etherpad",
|
||||||
component: EtherpadWidgetConfigComponent,
|
component: EtherpadWidgetConfigComponent,
|
||||||
@ -285,6 +293,7 @@ const routes: Routes = [
|
|||||||
{path: "generic", component: GenericWidgetWrapperComponent},
|
{path: "generic", component: GenericWidgetWrapperComponent},
|
||||||
{path: "video", component: VideoWidgetWrapperComponent},
|
{path: "video", component: VideoWidgetWrapperComponent},
|
||||||
{path: "jitsi", component: JitsiWidgetWrapperComponent},
|
{path: "jitsi", component: JitsiWidgetWrapperComponent},
|
||||||
|
{path: "bigbluebutton", component: BigBlueButtonWidgetWrapperComponent},
|
||||||
{path: "gcal", component: GCalWidgetWrapperComponent},
|
{path: "gcal", component: GCalWidgetWrapperComponent},
|
||||||
{path: "stickerpicker", component: StickerPickerWidgetWrapperComponent},
|
{path: "stickerpicker", component: StickerPickerWidgetWrapperComponent},
|
||||||
{path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent},
|
{path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent},
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
<my-widget-config [widgetComponent]="this">
|
||||||
|
<ng-template #widgetParamsTemplate let-widget="widget">
|
||||||
|
<label class="label-block">
|
||||||
|
BigBlueButton Meeting URL
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
placeholder="https://bbb.example.com/abc-def-ghi"
|
||||||
|
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
|
||||||
|
[disabled]="isUpdating"/>
|
||||||
|
</label>
|
||||||
|
</ng-template>
|
||||||
|
</my-widget-config>
|
@ -0,0 +1,53 @@
|
|||||||
|
import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component";
|
||||||
|
import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { FE_BigBlueButtonWidget } from "../../../shared/models/integration";
|
||||||
|
import { SessionStorage } from "../../../shared/SessionStorage";
|
||||||
|
import * as url from "url";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "bigbluebutton.widget.component.html",
|
||||||
|
styleUrls: ["bigbluebutton.widget.component.scss"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration of BigBlueButton widgets
|
||||||
|
export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||||
|
private bigBlueButtonWidget: FE_BigBlueButtonWidget = <FE_BigBlueButtonWidget>SessionStorage.editIntegration;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(WIDGET_BIGBLUEBUTTON, "BigBlueButton Conference", DISABLE_AUTOMATIC_WRAPPING);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OnWidgetsDiscovered(widgets: EditableWidget[]) {
|
||||||
|
for (const widget of widgets) {
|
||||||
|
widget.data.conferenceUrl = this.templateUrl(widget.url, widget.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OnNewWidgetPrepared(widget: EditableWidget): void {
|
||||||
|
widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OnWidgetBeforeAdd(widget: EditableWidget) {
|
||||||
|
this.setWidgetOptions(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OnWidgetBeforeEdit(widget: EditableWidget) {
|
||||||
|
this.setWidgetOptions(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setWidgetOptions(widget: EditableWidget) {
|
||||||
|
widget.dimension.newData.url = widget.dimension.newData.conferenceUrl;
|
||||||
|
|
||||||
|
let widgetQueryString = url.format({
|
||||||
|
query: {
|
||||||
|
"conferenceUrl": "$conferenceUrl",
|
||||||
|
"displayName": "$matrix_display_name",
|
||||||
|
"avatarUrl": "$matrix_avatar_url",
|
||||||
|
"userId": "$matrix_user_id",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
|
||||||
|
widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString;
|
||||||
|
}
|
||||||
|
}
|
@ -16,8 +16,8 @@
|
|||||||
<div class="info-box try-dimension shadowed">
|
<div class="info-box try-dimension shadowed">
|
||||||
<h3>Try it out or <a href="https://github.com/turt2live/matrix-dimension#running-your-own" target="_blank">run your own</a></h3>
|
<h3>Try it out or <a href="https://github.com/turt2live/matrix-dimension#running-your-own" target="_blank">run your own</a></h3>
|
||||||
<p>
|
<p>
|
||||||
Visit <a href="https://t2bot.io/riot" target="_blank">t2bot.io/riot</a> and log in with your Matrix account
|
Visit <a href="https://element.t2host.io" target="_blank">element.t2host.io</a> and log in with your Matrix account
|
||||||
or point your Riot <code>config.json</code> at our servers:
|
or point your Element <code>config.json</code> at our servers:
|
||||||
</p>
|
</p>
|
||||||
<pre>{{ integrationsConfig }}</pre>
|
<pre>{{ integrationsConfig }}</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -69,6 +69,10 @@
|
|||||||
<img src="/img/avatars/googlecalendar.png">
|
<img src="/img/avatars/googlecalendar.png">
|
||||||
<span>Google Calendar</span>
|
<span>Google Calendar</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="integration">
|
||||||
|
<img src="/img/avatars/bigbluebutton.png">
|
||||||
|
<span>BigBlueButton</span>
|
||||||
|
</div>
|
||||||
<div class="integration">
|
<div class="integration">
|
||||||
<img src="/img/avatars/customwidget.png">
|
<img src="/img/avatars/customwidget.png">
|
||||||
<span>Custom Widget</span>
|
<span>Custom Widget</span>
|
||||||
@ -220,19 +224,19 @@
|
|||||||
for news and updates. Don't forget to star the repository on
|
for news and updates. Don't forget to star the repository on
|
||||||
<a href="https://github.com/turt2live/matrix-dimension" target="_blank">GitHub</a>.
|
<a href="https://github.com/turt2live/matrix-dimension" target="_blank">GitHub</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>Here's the configuration options you'll need to update in your Riot <code>config.json</code>:</p>
|
<p>Here's the configuration options you'll need to update in your Element <code>config.json</code>:</p>
|
||||||
<pre>{{ integrationsConfig }}</pre>
|
<pre>{{ integrationsConfig }}</pre>
|
||||||
|
|
||||||
<h4>Configuring integrations</h4>
|
<h4>Configuring integrations</h4>
|
||||||
<p>
|
<p>
|
||||||
If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking
|
If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking
|
||||||
the 3x3 grid in the top right of any room in Riot. The gear icon (<i class="fa fa-cog"></i>) in the
|
the 3x3 grid in the top right of any room in Element. The gear icon (<i class="fa fa-cog"></i>) in the
|
||||||
top right is where you can configure your bots, bridges, and widgets.
|
top right is where you can configure your bots, bridges, and widgets.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4>"Could not connect to integrations server" error</h4>
|
<h4>"Could not connect to integrations server" error</h4>
|
||||||
<p>
|
<p>
|
||||||
When Riot cannot reach Dimension or Dimension is unable to reach your homeserver an error saying "Could not
|
When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying "Could not
|
||||||
contact integrations
|
contact integrations
|
||||||
server" shows up in every room. Before visiting us in <a href="https://matrix.to/#/#dimension:t2bot.io" target="_blank">#dimension:t2bot.io</a>
|
server" shows up in every room. Before visiting us in <a href="https://matrix.to/#/#dimension:t2bot.io" target="_blank">#dimension:t2bot.io</a>
|
||||||
on Matrix, here's a few things to check:
|
on Matrix, here's a few things to check:
|
||||||
|
@ -11,7 +11,7 @@ export class HomeComponent {
|
|||||||
public showPromoPage = this.hostname === "https://dimension.t2bot.io";
|
public showPromoPage = this.hostname === "https://dimension.t2bot.io";
|
||||||
|
|
||||||
public integrationsConfig = `` +
|
public integrationsConfig = `` +
|
||||||
`"integrations_ui_url": "${this.hostname}/riot",\n` +
|
`"integrations_ui_url": "${this.hostname}/element",\n` +
|
||||||
`"integrations_rest_url": "${this.hostname}/api/v1/scalar",\n` +
|
`"integrations_rest_url": "${this.hostname}/api/v1/scalar",\n` +
|
||||||
`"integrations_widgets_urls": ["${this.hostname}/widgets"],\n` +
|
`"integrations_widgets_urls": ["${this.hostname}/widgets"],\n` +
|
||||||
`"integrations_jitsi_widget_url": "${this.hostname}/widgets/jitsi",\n`;
|
`"integrations_jitsi_widget_url": "${this.hostname}/widgets/jitsi",\n`;
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
<strong>Integrations are not encrypted!</strong>
|
<strong>Integrations are not encrypted!</strong>
|
||||||
This means that some information about yourself and the
|
This means that some information about yourself and the
|
||||||
room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
|
room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
|
||||||
name, your username, your avatar, information about Riot, and other similar details. Add integrations
|
name, your username, your avatar, information about Element, and other similar details. Add integrations
|
||||||
with caution.
|
with caution.
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-warning" *ngIf="!hasIntegrations() && isRoomEncrypted">
|
<div class="alert alert-warning" *ngIf="!hasIntegrations() && isRoomEncrypted">
|
||||||
|
@ -77,7 +77,7 @@ export class RiotHomeComponent {
|
|||||||
console.error("No user returned for token. Is the token registered in Dimension?");
|
console.error("No user returned for token. Is the token registered in Dimension?");
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.errorMessage = "Could not verify your token. Please try logging out of Riot and back in. Be sure to back up your encryption keys!";
|
this.errorMessage = "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!";
|
||||||
} else {
|
} else {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
console.log("Scalar token belongs to " + userId);
|
console.log("Scalar token belongs to " + userId);
|
||||||
@ -189,7 +189,7 @@ export class RiotHomeComponent {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.errorMessage = "Unable to set up Dimension. This version of Riot may not supported or there may be a problem with the server.";
|
this.errorMessage = "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.";
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stickerApi.getPacks().then(packs => {
|
this.stickerApi.getPacks().then(packs => {
|
||||||
@ -265,7 +265,7 @@ export class RiotHomeComponent {
|
|||||||
case "publicRoom":
|
case "publicRoom":
|
||||||
return this.scalar.getJoinRule(this.roomId).then(payload => {
|
return this.scalar.getJoinRule(this.roomId).then(payload => {
|
||||||
if (!payload.response) {
|
if (!payload.response) {
|
||||||
return Promise.reject("Could not communicate with Riot");
|
return Promise.reject("Could not communicate with Element");
|
||||||
}
|
}
|
||||||
const isPublic = payload.response.join_rule === "public";
|
const isPublic = payload.response.join_rule === "public";
|
||||||
if (isPublic !== requirement.expectedValue) {
|
if (isPublic !== requirement.expectedValue) {
|
||||||
@ -278,7 +278,7 @@ export class RiotHomeComponent {
|
|||||||
if (response === true) return Promise.resolve();
|
if (response === true) return Promise.resolve();
|
||||||
if (response.error || response.error.message)
|
if (response.error || response.error.message)
|
||||||
return Promise.reject("You cannot modify widgets in this room");
|
return Promise.reject("You cannot modify widgets in this room");
|
||||||
return Promise.reject("Error communicating with Riot");
|
return Promise.reject("Error communicating with Element");
|
||||||
};
|
};
|
||||||
|
|
||||||
let promiseChain = Promise.resolve();
|
let promiseChain = Promise.resolve();
|
||||||
|
@ -64,6 +64,11 @@ export interface FE_Sticker {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FE_BigBlueButtonJoin {
|
||||||
|
// The meeting URL the client should load to join the meeting
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FE_StickerConfig {
|
export interface FE_StickerConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
stickerBot: string;
|
stickerBot: string;
|
||||||
@ -88,6 +93,12 @@ export interface FE_JitsiWidget extends FE_Widget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FE_BigBlueButtonWidget extends FE_Widget {
|
||||||
|
options: {
|
||||||
|
conferenceUrl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface FE_IntegrationRequirement {
|
export interface FE_IntegrationRequirement {
|
||||||
condition: "publicRoom" | "canSendEventTypes" | "userInRoom";
|
condition: "publicRoom" | "canSendEventTypes" | "userInRoom";
|
||||||
argument: any;
|
argument: any;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { WidgetsResponse } from "./server-client-responses";
|
import { WidgetsResponse } from "./server-client-responses";
|
||||||
|
|
||||||
export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"];
|
export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"];
|
||||||
|
export const WIDGET_BIGBLUEBUTTON = ["bigbluebutton", "dimension-bigbluebutton"];
|
||||||
export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"];
|
export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"];
|
||||||
export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"];
|
export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"];
|
||||||
export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"];
|
export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"];
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
WIDGET_CUSTOM,
|
WIDGET_CUSTOM,
|
||||||
|
WIDGET_BIGBLUEBUTTON,
|
||||||
WIDGET_ETHERPAD,
|
WIDGET_ETHERPAD,
|
||||||
WIDGET_GOOGLE_CALENDAR,
|
WIDGET_GOOGLE_CALENDAR,
|
||||||
WIDGET_GOOGLE_DOCS,
|
WIDGET_GOOGLE_DOCS,
|
||||||
@ -35,6 +36,9 @@ export class IntegrationsRegistry {
|
|||||||
"custom": {
|
"custom": {
|
||||||
types: WIDGET_CUSTOM,
|
types: WIDGET_CUSTOM,
|
||||||
},
|
},
|
||||||
|
"bigbluebutton": {
|
||||||
|
types: WIDGET_BIGBLUEBUTTON,
|
||||||
|
},
|
||||||
"youtube": {
|
"youtube": {
|
||||||
types: WIDGET_YOUTUBE
|
types: WIDGET_YOUTUBE
|
||||||
},
|
},
|
||||||
|
@ -20,4 +20,8 @@ export class AdminStickersApiService extends AuthedApi {
|
|||||||
public importFromTelegram(packUrl: string): Promise<FE_StickerPack> {
|
public importFromTelegram(packUrl: string): Promise<FE_StickerPack> {
|
||||||
return this.authedPost<FE_StickerPack>("/api/v1/dimension/admin/stickers/packs/import/telegram", {packUrl: packUrl}).toPromise();
|
return this.authedPost<FE_StickerPack>("/api/v1/dimension/admin/stickers/packs/import/telegram", {packUrl: packUrl}).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removePack(packId: number): Promise<any> {
|
||||||
|
return this.authedDelete("/api/v1/dimension/admin/stickers/packs/" + packId).toPromise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { AuthedApi } from "../authed-api";
|
||||||
|
import { FE_BigBlueButtonJoin } from "../../models/integration"
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { ApiError } from "../../../../../src/api/ApiError";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BigBlueButtonApiService extends AuthedApi {
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
super(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
public joinMeeting(url: string, name: string): Promise<FE_BigBlueButtonJoin|ApiError> {
|
||||||
|
return this.authedGet<FE_BigBlueButtonJoin|ApiError>("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise();
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ export class ScalarWidgetApi {
|
|||||||
|
|
||||||
public static sendSetAlwaysOnScreen(alwaysVisible: boolean): void {
|
public static sendSetAlwaysOnScreen(alwaysVisible: boolean): void {
|
||||||
ScalarWidgetApi.callAction("set_always_on_screen", {
|
ScalarWidgetApi.callAction("set_always_on_screen", {
|
||||||
// Send the value here and in data due to a Riot bug.
|
// Send the value here and in data due to a Element bug.
|
||||||
data: {
|
data: {
|
||||||
value: alwaysVisible,
|
value: alwaysVisible,
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<iframe *ngIf="embedUrl"
|
||||||
|
id="bigBlueButtonContainer"
|
||||||
|
[src]="embedUrl"
|
||||||
|
(load)="onIframeLoad()"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
allow="camera; microphone; encrypted-media; autoplay;"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
<div *ngIf="!embedUrl" class="join-conference-wrapper">
|
||||||
|
<div class="join-conference-boat">
|
||||||
|
<div *ngIf="statusMessage; else joinMeetingPrompt" class="join-conference-prompt">
|
||||||
|
<h4 [innerHTML]="statusMessage"></h4>
|
||||||
|
</div>
|
||||||
|
<ng-template #joinMeetingPrompt>
|
||||||
|
<div class="join-conference-prompt">
|
||||||
|
<h3>BigBlueButton Conference</h3>
|
||||||
|
<button type="button" (click)="joinConference()" class="btn btn-primary btn-large">
|
||||||
|
Join Conference
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,32 @@
|
|||||||
|
// component styles are encapsulated and only applied to their components
|
||||||
|
@import "../../../style/themes/themes";
|
||||||
|
|
||||||
|
@include themifyComponent() {
|
||||||
|
#bigBlueButtonContainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-conference-wrapper {
|
||||||
|
display: table;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: themed(widgetWelcomeBgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-conference-boat {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-conference-prompt {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
152
web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import { WidgetApiService } from "../../shared/services/integrations/widget-api.service";
|
||||||
|
import { Subscription } from "rxjs/Subscription";
|
||||||
|
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
|
||||||
|
import { CapableWidget } from "../capable-widget";
|
||||||
|
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
||||||
|
import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service";
|
||||||
|
import { FE_BigBlueButtonJoin } from "../../shared/models/integration";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "my-bigbluebutton-widget-wrapper",
|
||||||
|
templateUrl: "bigbluebutton.component.html",
|
||||||
|
styleUrls: ["bigbluebutton.component.scss"],
|
||||||
|
})
|
||||||
|
export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
public canEmbed = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User metadata passed to us by the client
|
||||||
|
*/
|
||||||
|
private conferenceUrl: string;
|
||||||
|
private displayName: string;
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The poll period in ms while waiting for a meeting to start
|
||||||
|
*/
|
||||||
|
private pollIntervalMillis = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscriber for messages from the client via the postMessage API
|
||||||
|
*/
|
||||||
|
private bigBlueButtonApiSubscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A status message to display to the user in the widget, typically for loading messages
|
||||||
|
*/
|
||||||
|
public statusMessage: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we are currently in a meeting
|
||||||
|
*/
|
||||||
|
private inMeeting: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to embed into the iframe
|
||||||
|
*/
|
||||||
|
public embedUrl: SafeUrl = null;
|
||||||
|
|
||||||
|
constructor(activatedRoute: ActivatedRoute,
|
||||||
|
private bigBlueButtonApi: BigBlueButtonApiService,
|
||||||
|
private widgetApi: WidgetApiService,
|
||||||
|
private sanitizer: DomSanitizer) {
|
||||||
|
super();
|
||||||
|
this.supportsAlwaysOnScreen = true;
|
||||||
|
|
||||||
|
let params: any = activatedRoute.snapshot.queryParams;
|
||||||
|
|
||||||
|
console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl);
|
||||||
|
|
||||||
|
this.conferenceUrl = params.conferenceUrl;
|
||||||
|
this.displayName = params.displayName;
|
||||||
|
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
|
||||||
|
|
||||||
|
// Set the widget ID if we have it
|
||||||
|
ScalarWidgetApi.widgetId = params.widgetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onIframeLoad() {
|
||||||
|
if (this.inMeeting) {
|
||||||
|
// The meeting has ended and we've come back full circle
|
||||||
|
this.inMeeting = false;
|
||||||
|
this.statusMessage = null;
|
||||||
|
this.embedUrl = null;
|
||||||
|
|
||||||
|
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have a toggle for whether we're in a meeting. We do this as we don't have a method
|
||||||
|
// of checking which URL was just loaded in the iframe (due to different origin domains
|
||||||
|
// and browser security), so we have to guess that it'll always be the second load (the
|
||||||
|
// first being joining the meeting)
|
||||||
|
this.inMeeting = true;
|
||||||
|
|
||||||
|
// We've successfully joined the meeting
|
||||||
|
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public joinConference(updateStatusMessage: boolean = true) {
|
||||||
|
if (updateStatusMessage) {
|
||||||
|
// Inform the user that we're loading their meeting
|
||||||
|
this.statusMessage = "Joining conference...";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a nick to display in the meeting
|
||||||
|
const joinName = `${this.displayName} (${this.userId})`;
|
||||||
|
|
||||||
|
// Make a request to Dimension requesting the join URL
|
||||||
|
console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl);
|
||||||
|
this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).then((response) => {
|
||||||
|
if ("errorCode" in response) {
|
||||||
|
// This is an instance of ApiError
|
||||||
|
if (response.errorCode == "WAITING_FOR_MEETING_START") {
|
||||||
|
// The meeting hasn't started yet
|
||||||
|
this.statusMessage = "Waiting for conference to start...";
|
||||||
|
|
||||||
|
// Poll until it has
|
||||||
|
setTimeout(this.joinConference.bind(this), this.pollIntervalMillis, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise this is a generic error
|
||||||
|
this.statusMessage = "An error occurred while loading the meeting";
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinUrl = (response as FE_BigBlueButtonJoin).url;
|
||||||
|
|
||||||
|
// Check if the given URL is embeddable
|
||||||
|
this.widgetApi.isEmbeddable(joinUrl).then(result => {
|
||||||
|
this.canEmbed = result.canEmbed;
|
||||||
|
this.statusMessage = null;
|
||||||
|
|
||||||
|
// Embed the return meeting URL, joining the meeting
|
||||||
|
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl);
|
||||||
|
|
||||||
|
// Inform the client that we would like the meeting to remain visible for its duration
|
||||||
|
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this.canEmbed = false;
|
||||||
|
this.statusMessage = "Unable to embed meeting";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
if (this.bigBlueButtonApiSubscription) this.bigBlueButtonApiSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onCapabilitiesSent(): void {
|
||||||
|
super.onCapabilitiesSent();
|
||||||
|
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: themed(jitsiWelcomeBgColor);
|
background-color: themed(widgetWelcomeBgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-conference-boat {
|
.join-conference-boat {
|
||||||
|
@ -38,9 +38,8 @@ export class JitsiWidgetWrapperComponent extends CapableWidget implements OnInit
|
|||||||
this.conferenceId = params.conferenceId || params.confId;
|
this.conferenceId = params.conferenceId || params.confId;
|
||||||
this.displayName = params.displayName;
|
this.displayName = params.displayName;
|
||||||
this.avatarUrl = params.avatarUrl;
|
this.avatarUrl = params.avatarUrl;
|
||||||
this.userId = params.userId || params.email; // Riot uses `email` when placing a conference call
|
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
|
||||||
this.isAudioOnly = params.isAudioOnly === 'true';
|
this.isAudioOnly = params.isAudioOnly === 'true';
|
||||||
|
|
||||||
this.toggleVideo = !this.isAudioOnly;
|
this.toggleVideo = !this.isAudioOnly;
|
||||||
|
|
||||||
// Set the widget ID if we have it
|
// Set the widget ID if we have it
|
||||||
@ -54,7 +53,7 @@ export class JitsiWidgetWrapperComponent extends CapableWidget implements OnInit
|
|||||||
$.getScript(widget.options.scriptUrl);
|
$.getScript(widget.options.scriptUrl);
|
||||||
|
|
||||||
if (!this.domain) {
|
if (!this.domain) {
|
||||||
// Always fall back to jitsi.riot.im to maintain compatibility with widgets created by Riot.
|
// Always fall back to jitsi.riot.im to maintain compatibility with widgets created by Element.
|
||||||
this.domain = widget.options.useDomainAsDefault ? widget.options.jitsiDomain : "jitsi.riot.im";
|
this.domain = widget.options.useDomainAsDefault ? widget.options.jitsiDomain : "jitsi.riot.im";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<button class="btn btn-link btn-sm" (click)="openIntegrationManager()">Add some stickers</button>
|
<button class="btn btn-link btn-sm" (click)="openIntegrationManager()">Add some stickers</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sticker-picker" *ngIf="!isLoading && !authError">
|
<div class="sticker-picker" *ngIf="!isLoading && !authError">
|
||||||
<div class="sticker-pack" *ngFor="let pack of packs trackById">
|
<div class="sticker-pack" *ngFor="let pack of packs trackById" [attr.id]="'pack-' + pack.id">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="title">{{ pack.displayName }}</span>
|
<span class="title">{{ pack.displayName }}</span>
|
||||||
<span class="license"><a [href]="pack.license.urlPath"
|
<span class="license"><a [href]="pack.license.urlPath"
|
||||||
@ -32,5 +32,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sticker-pack-list" [@hideList]="isListVisible ? 'visible' : 'hidden'" (wheel)="scrollHorizontal($event)" >
|
||||||
|
<div class="sticker-pack-list-item" *ngFor="let pack of packs trackById" (click)="scrollToPack('pack-' + pack.id)">
|
||||||
|
<img [src]="getThumbnailUrl(pack.stickers[0].thumbnail.mxc, 48, 48)" width="40" height="40" class="image"
|
||||||
|
[alt]="pack.displayName" [ngbTooltip]="pack.displayName" placement="top" container="body"/>
|
||||||
|
</div>
|
||||||
|
<div class="sticker-pack-list-config" (click)="openIntegrationManager()"
|
||||||
|
ngbTooltip="Settings" placement="top" container="body">
|
||||||
|
<i class="fas fa-2x fa-cog"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
.sticker-picker {
|
.sticker-picker {
|
||||||
margin: 15px 15px 30px;
|
margin: 15px 15px 30px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
.sticker-pack {
|
.sticker-pack {
|
||||||
.header {
|
.header {
|
||||||
@ -92,5 +93,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticker-pack-list {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: themed(stickerPickerControlBgColor);
|
||||||
|
border-top: 1px solid themed(stickerPickerShadowColor);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 1px 15px;
|
||||||
|
|
||||||
|
.sticker-pack-list-item {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticker-pack-list-config {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 3px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,22 @@
|
|||||||
|
import {
|
||||||
|
animate,
|
||||||
|
state,
|
||||||
|
style,
|
||||||
|
transition,
|
||||||
|
trigger
|
||||||
|
} from '@angular/animations';
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { CapableWidget, WIDGET_API_VERSION_OPENID } from "../capable-widget";
|
import { CapableWidget, WIDGET_API_VERSION_OPENID } from "../capable-widget";
|
||||||
|
import { fromEvent } from 'rxjs';
|
||||||
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
pairwise,
|
||||||
|
share,
|
||||||
|
throttleTime
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { Subscription } from "rxjs/Subscription";
|
import { Subscription } from "rxjs/Subscription";
|
||||||
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
|
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
|
||||||
import { StickerApiService } from "../../shared/services/integrations/sticker-api.service";
|
import { StickerApiService } from "../../shared/services/integrations/sticker-api.service";
|
||||||
@ -14,10 +30,25 @@ import { WIDGET_STICKER_PICKER } from "../../shared/models/widget";
|
|||||||
selector: "my-generic-widget-wrapper",
|
selector: "my-generic-widget-wrapper",
|
||||||
templateUrl: "sticker-picker.component.html",
|
templateUrl: "sticker-picker.component.html",
|
||||||
styleUrls: ["sticker-picker.component.scss"],
|
styleUrls: ["sticker-picker.component.scss"],
|
||||||
|
animations: [
|
||||||
|
trigger('hideList', [
|
||||||
|
state(
|
||||||
|
'hidden',
|
||||||
|
style({ opacity: 0, transform: 'translateY(100%)' })
|
||||||
|
),
|
||||||
|
state(
|
||||||
|
'visible',
|
||||||
|
style({ opacity: 1, transform: 'translateY(0)' })
|
||||||
|
),
|
||||||
|
transition('* => *', animate('200ms ease-in'))
|
||||||
|
])
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class StickerPickerWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy {
|
export class StickerPickerWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy {
|
||||||
|
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
public isListVisible = true;
|
||||||
public authError = false;
|
public authError = false;
|
||||||
public packs: FE_UserStickerPack[];
|
public packs: FE_UserStickerPack[];
|
||||||
|
|
||||||
@ -68,6 +99,28 @@ export class StickerPickerWidgetWrapperComponent extends CapableWidget implement
|
|||||||
if (this.stickerWidgetApiSubscription) this.stickerWidgetApiSubscription.unsubscribe();
|
if (this.stickerWidgetApiSubscription) this.stickerWidgetApiSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit() {
|
||||||
|
const scroll$ = fromEvent(window, 'scroll').pipe(
|
||||||
|
throttleTime(10),
|
||||||
|
map(() => window.pageYOffset),
|
||||||
|
pairwise(),
|
||||||
|
map(([y1, y2]): string => (y2 < y1 ? 'up' : 'down')),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
share()
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollUp$ = scroll$.pipe(
|
||||||
|
filter(direction => direction === 'up')
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollDown = scroll$.pipe(
|
||||||
|
filter(direction => direction === 'down')
|
||||||
|
);
|
||||||
|
|
||||||
|
scrollUp$.subscribe(() => (this.isListVisible = true));
|
||||||
|
scrollDown.subscribe(() => (this.isListVisible = false));
|
||||||
|
}
|
||||||
|
|
||||||
protected onSupportedVersionsFound(): void {
|
protected onSupportedVersionsFound(): void {
|
||||||
super.onSupportedVersionsFound();
|
super.onSupportedVersionsFound();
|
||||||
|
|
||||||
@ -133,6 +186,16 @@ export class StickerPickerWidgetWrapperComponent extends CapableWidget implement
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public scrollHorizontal(event: WheelEvent): void {
|
||||||
|
document.getElementsByClassName('sticker-pack-list')[0].scrollLeft += event.deltaY;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollToPack(id: string) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.scrollIntoView({behavior: 'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
public sendSticker(sticker: FE_Sticker, pack: FE_UserStickerPack) {
|
public sendSticker(sticker: FE_Sticker, pack: FE_UserStickerPack) {
|
||||||
ScalarWidgetApi.sendSticker(sticker, pack);
|
ScalarWidgetApi.sendSticker(sticker, pack);
|
||||||
}
|
}
|
||||||
|
BIN
web/public/img/avatars/bigbluebutton.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1,15 +1 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="684" height="684" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 513 513"><metadata>Created by potrace 1.11, written by Peter Selinger 2001-2013</metadata><g fill="#000" stroke="none"><path d="M0 2565 l0 -2565 2565 0 2565 0 0 2565 0 2565 -2565 0 -2565 0 0 -2565z" transform="translate(0.000000,513.000000) scale(0.100000,-0.100000)"/></g></svg>
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="513.000000pt" height="513.000000pt" viewBox="0 0 513.000000 513.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,513.000000) scale(0.100000,-0.100000)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M0 2565 l0 -2565 2565 0 2565 0 0 2565 0 2565 -2565 0 -2565 0 0
|
|
||||||
-2565z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 407 B |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 68 KiB |
@ -1,4 +1,4 @@
|
|||||||
// The CSS for the Riot breadcrumb is specified here to ensure that it's style can be overridden.
|
// The CSS for the Element breadcrumb is specified here to ensure that it's style can be overridden.
|
||||||
// In it's current position (as a component), the component-level stylesheet cannot access the
|
// In it's current position (as a component), the component-level stylesheet cannot access the
|
||||||
// elements, so we specify it in a more generic location.
|
// elements, so we specify it in a more generic location.
|
||||||
@import "themes/themes";
|
@import "themes/themes";
|
||||||
|
@ -48,7 +48,7 @@ $theme_dark: (
|
|||||||
stickerPickerStickerBgColor: #fff,
|
stickerPickerStickerBgColor: #fff,
|
||||||
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
|
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
|
||||||
|
|
||||||
jitsiWelcomeBgColor: #fff,
|
widgetWelcomeBgColor: #fff,
|
||||||
|
|
||||||
troubleshooterBgColor: #2d2d2d,
|
troubleshooterBgColor: #2d2d2d,
|
||||||
troubleshooterNeutralColor: rgb(205, 215, 222),
|
troubleshooterNeutralColor: rgb(205, 215, 222),
|
||||||
|
@ -48,7 +48,7 @@ $theme_light: (
|
|||||||
stickerPickerStickerBgColor: #fff,
|
stickerPickerStickerBgColor: #fff,
|
||||||
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
|
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
|
||||||
|
|
||||||
jitsiWelcomeBgColor: #fff,
|
widgetWelcomeBgColor: #fff,
|
||||||
|
|
||||||
troubleshooterBgColor: #fff,
|
troubleshooterBgColor: #fff,
|
||||||
troubleshooterNeutralColor: rgb(205, 215, 222),
|
troubleshooterNeutralColor: rgb(205, 215, 222),
|
||||||
|