diff --git a/config/bot.json b/config/bot.json deleted file mode 100644 index a46a1f9..0000000 --- a/config/bot.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "syncToken": "s29_4025_0_1_40_1_26_19_1", - "filter": null, - "appserviceUsers": {}, - "appserviceTransactions": {}, - "kvStore": {} -} \ No newline at end of file diff --git a/config/bot.json.bak b/config/bot.json.bak deleted file mode 100644 index 5735938..0000000 --- a/config/bot.json.bak +++ /dev/null @@ -1,6 +0,0 @@ -{ - "syncToken": "s3510138_39116385_25484_5602626_218935_269_119102_8310593_32", - "filter": null, - "appserviceUsers": {}, - "appserviceTransactions": {} -} \ No newline at end of file diff --git a/config/dimension.db.bak b/config/dimension.db.bak deleted file mode 100644 index 112c0d7..0000000 Binary files a/config/dimension.db.bak and /dev/null differ diff --git a/config/dimension.db.bak2 b/config/dimension.db.bak2 deleted file mode 100644 index 6bf025f..0000000 Binary files a/config/dimension.db.bak2 and /dev/null differ diff --git a/docs/local.md b/docs/local.md new file mode 100644 index 0000000..dcff9ba --- /dev/null +++ b/docs/local.md @@ -0,0 +1,122 @@ +# Developer/local environment setup + +**Disclaimer**: This guide assumes a fairly high level of pre-existing knowledge regarding development practices, Matrix, +integration managers, and troubleshooting. The guide covers the happy path, though sometimes it doesn't work out that way. +Troubleshooting the system is best before jumping into support rooms: it helps build context on how all this works. + +Prerequisites: +* A local Synapse. +* A local Element Web. +* All the other dependencies which would be required by a production install. + +**Note**: As a local environment, you do not need to use https for any of the setup. In fact, it is recommended to stick +to plain http as it involves less certificate issues. For added benefit, set your homeserver name to `localhost`. + +## Part 1: Configuration + +Copy the default configuration and give it an edit. The major settings will be the `web`, `homeserver`, and `admins` +sections. + +It is recommended to run Dimension on `0.0.0.0:8184` for demonstration purposes. Your homeserver's `clientServerUrl` +*and* `federationUrl` should be pointed to your local Synapse. This will bypass a lot of the federation-level stuff +by pointing your Dimension directly at the Synapse instance rather than it trying to resolve `localhost` on the public +federation. + +The `accessToken` should be a dedicated user **that has not ever opened Dimension**. Set this up now before configuring +the client. + +```bash +# Edit the configuration to your specifications. +# Be sure to add yourself as an admin! +cp config/default.yaml config/development.yaml +nano config/development.yaml +``` + +## Part 2: Installation and running + +In a bash terminal: + +```bash +npm install +npm run start:apponly +``` + +If that starts up without issue (ie: doesn't crash) then open a second terminal with: + +```bash +npm run start:web +``` + +You should now have **2** terminals, one for the web app and one for the backend. Both should live reload if you make +changes to the respective layers. + +The web server will start on port `8082`. + +## Part 3: Element configuration + +In your local `config.json`, add/edit the following keys: + +```json +{ + "integrations_ui_url": "http://localhost:8082/element", + "integrations_rest_url": "http://localhost:8082/api/v1/scalar", + "integrations_widgets_urls": ["http://localhost:8082/widgets"], + "integrations_jitsi_widget_url": "http://localhost:8082/widgets/jitsi" +} +``` + +## Part 4: Using Dimension + +If everything went according to plan, Dimension should now be accessible from the "Add widgets, bridges & bots" button on +the room information card. + +You should see a cog/gear icon in the top right of the dialog window. This indicates that you are an admin. If you do not +see this, fix the Dimension config and restart the `npm run start:apponly` process. + +## Part 5: Configuring integrations in Dimension + +Click the gear icon in the Dimension window within Element. The menu on the left side should guide you through the process +of setting up and configuring integrations. + +**Danger**: Upstream (matrix.org) integrations will not work. Do not enable them in local environments. + +**Note**: Dimension enforces a maximum of one bridge instance per type. It does this by disabling buttons despite indicating +that there's potential for multiple to be added. Adding a self-hosted bridge will disable all future options of adding a +second bridge, but it should be possible to edit the added instance. + +For the purposes of this guide, we'll set up Hookshot and a custom bot. + +## Part 6: Hookshot + +First, set up a local instance of [matrix-hookshot](https://github.com/Half-Shot/matrix-hookshot) within your test environment. +It is recommended to proxy some of the endpoints through ngrok (or similar) to make external integration a lot easier. + +For testing purposes it's often best to use a single Hookshot instance, though Dimension will assume that you'll have +multiple instances, one for each capable service, however it is perfectly fine to point all the Dimension configs at the +same Hookshot instance. + +The Hookshot-capable bridges can be configured from here: +![](https://i.imgur.com/42dTDuk.png) +*UI may vary. Note the subtle difference in descriptions for the two webhook bridges.* + +Simply configure the bridges from that view and click "Browse integrations" in the top left to go work with the bridges. +They should appear as integrations within Dimension. + +## Part 7: Custom bots + +Custom bots can be configured from the admin section under "Custom bots". Dimension will need an access token, and will +assume that the bots do not need special power levels or invite handling - they will be required to operate at the default +power level for rooms, and auto-accept invites on their own. Dimension will take care of removing the bot from the room +when the time comes, and the bot should be okay with this happening outside of its process. + +Maubot bots are great options for inclusion here. + +## Part 8: Troubleshooting + +Occasionally the live reload functionality either doesn't work or causes problems. Re-open Dimension within Element to +fix most issues, and restart the relevant processes if needed. + +Note that Dimension will do its best to avoid crashing, but will produce cryptic errors if an integration is down or +misconfigured. + +For all other troubleshooting, please visit the support room. diff --git a/package.json b/package.json index d64c6a3..81fbc20 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "lint:app": "eslint src", "lint:web": "eslint web", "i18n": "npm run-script i18n:init && npm run-script i18n:extract", - "i18n:init": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/template.json --key-as-default-value --replace --format json", - "i18n:extract": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/en.json --clean --format json" + "i18n:init": "ngx-translate-extract --input ./web --output ./web/assets/i18n/template.json --key-as-default-value --replace --format json", + "i18n:extract": "ngx-translate-extract --input ./web --output ./web/assets/i18n/en.json --key-as-default-value --clean --format json" }, "repository": { "type": "git", diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index f856771..3883844 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -51,6 +51,9 @@ export const CACHE_FEDERATION = "federation"; export const CACHE_IRC_BRIDGE = "irc-bridge"; export const CACHE_STICKERS = "stickers"; export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge"; +export const CACHE_HOOKSHOT_GITHUB_BRIDGE = "hookshot-github-bridge"; +export const CACHE_HOOKSHOT_WEBHOOK_BRIDGE = "hookshot-webhook-bridge"; +export const CACHE_HOOKSHOT_JIRA_BRIDGE = "hookshot-jira-bridge"; export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge"; export const CACHE_SIMPLE_BOTS = "simple-bots"; export const CACHE_SLACK_BRIDGE = "slack-bridge"; diff --git a/src/api/admin/AdminHookshotGithubService.ts b/src/api/admin/AdminHookshotGithubService.ts new file mode 100644 index 0000000..c12dac3 --- /dev/null +++ b/src/api/admin/AdminHookshotGithubService.ts @@ -0,0 +1,110 @@ +import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest"; +import { Cache, CACHE_HOOKSHOT_GITHUB_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache"; +import { LogService } from "matrix-bot-sdk"; +import { ApiError } from "../ApiError"; +import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity"; +import HookshotGithubBridgeRecord from "../../db/models/HookshotGithubBridgeRecord"; + +interface CreateWithUpstream { + upstreamId: number; +} + +interface CreateSelfhosted { + provisionUrl: string; + sharedSecret: string; +} + +interface BridgeResponse { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +/** + * Administrative API for configuring Hookshot Github bridge instances. + */ +@Path("/api/v1/dimension/admin/hookshot/github") +export class AdminHookshotGithubService { + + @Context + private context: ServiceContext; + + @GET + @Path("all") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridges(): Promise { + const bridges = await HookshotGithubBridgeRecord.findAll(); + return Promise.all(bridges.map(async b => { + return { + id: b.id, + upstreamId: b.upstreamId, + provisionUrl: b.provisionUrl, + sharedSecret: b.sharedSecret, + isEnabled: b.isEnabled, + }; + })); + } + + @GET + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise { + const githubBridge = await HookshotGithubBridgeRecord.findByPk(bridgeId); + if (!githubBridge) throw new ApiError(404, "Github Bridge not found"); + + return { + id: githubBridge.id, + upstreamId: githubBridge.upstreamId, + provisionUrl: githubBridge.provisionUrl, + sharedSecret: githubBridge.sharedSecret, + isEnabled: githubBridge.isEnabled, + }; + } + + @POST + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotGithubBridgeRecord.findByPk(bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + bridge.provisionUrl = request.provisionUrl; + bridge.sharedSecret = request.sharedSecret; + await bridge.save(); + + LogService.info("AdminHookshotGithubService", userId + " updated Hookshot Github Bridge " + bridge.id); + + Cache.for(CACHE_HOOKSHOT_GITHUB_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } + + @POST + @Path("new/upstream") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise { + throw new ApiError(400, "Cannot create a github bridge from an upstream"); + } + + @POST + @Path("new/selfhosted") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newSelfhosted(request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotGithubBridgeRecord.create({ + provisionUrl: request.provisionUrl, + sharedSecret: request.sharedSecret, + isEnabled: true, + }); + LogService.info("AdminHookshotGithubService", userId + " created a new Hookshot Github Bridge with provisioning URL " + request.provisionUrl); + + Cache.for(CACHE_HOOKSHOT_GITHUB_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } +} diff --git a/src/api/admin/AdminHookshotJiraService.ts b/src/api/admin/AdminHookshotJiraService.ts new file mode 100644 index 0000000..057a5cc --- /dev/null +++ b/src/api/admin/AdminHookshotJiraService.ts @@ -0,0 +1,110 @@ +import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest"; +import { Cache, CACHE_HOOKSHOT_JIRA_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache"; +import { LogService } from "matrix-bot-sdk"; +import { ApiError } from "../ApiError"; +import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity"; +import HookshotJiraBridgeRecord from "../../db/models/HookshotJiraBridgeRecord"; + +interface CreateWithUpstream { + upstreamId: number; +} + +interface CreateSelfhosted { + provisionUrl: string; + sharedSecret: string; +} + +interface BridgeResponse { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +/** + * Administrative API for configuring Hookshot Jira bridge instances. + */ +@Path("/api/v1/dimension/admin/hookshot/jira") +export class AdminHookshotJiraService { + + @Context + private context: ServiceContext; + + @GET + @Path("all") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridges(): Promise { + const bridges = await HookshotJiraBridgeRecord.findAll(); + return Promise.all(bridges.map(async b => { + return { + id: b.id, + upstreamId: b.upstreamId, + provisionUrl: b.provisionUrl, + sharedSecret: b.sharedSecret, + isEnabled: b.isEnabled, + }; + })); + } + + @GET + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise { + const jiraBridge = await HookshotJiraBridgeRecord.findByPk(bridgeId); + if (!jiraBridge) throw new ApiError(404, "Jira Bridge not found"); + + return { + id: jiraBridge.id, + upstreamId: jiraBridge.upstreamId, + provisionUrl: jiraBridge.provisionUrl, + sharedSecret: jiraBridge.sharedSecret, + isEnabled: jiraBridge.isEnabled, + }; + } + + @POST + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotJiraBridgeRecord.findByPk(bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + bridge.provisionUrl = request.provisionUrl; + bridge.sharedSecret = request.sharedSecret; + await bridge.save(); + + LogService.info("AdminHookshotJiraService", userId + " updated Hookshot Jira Bridge " + bridge.id); + + Cache.for(CACHE_HOOKSHOT_JIRA_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } + + @POST + @Path("new/upstream") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise { + throw new ApiError(400, "Cannot create a jira bridge from an upstream"); + } + + @POST + @Path("new/selfhosted") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newSelfhosted(request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotJiraBridgeRecord.create({ + provisionUrl: request.provisionUrl, + sharedSecret: request.sharedSecret, + isEnabled: true, + }); + LogService.info("AdminHookshotJiraService", userId + " created a new Hookshot Jira Bridge with provisioning URL " + request.provisionUrl); + + Cache.for(CACHE_HOOKSHOT_JIRA_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } +} diff --git a/src/api/admin/AdminHookshotWebhookService.ts b/src/api/admin/AdminHookshotWebhookService.ts new file mode 100644 index 0000000..bfbd58f --- /dev/null +++ b/src/api/admin/AdminHookshotWebhookService.ts @@ -0,0 +1,110 @@ +import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest"; +import { Cache, CACHE_HOOKSHOT_WEBHOOK_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache"; +import { LogService } from "matrix-bot-sdk"; +import { ApiError } from "../ApiError"; +import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity"; +import HookshotWebhookBridgeRecord from "../../db/models/HookshotWebhookBridgeRecord"; + +interface CreateWithUpstream { + upstreamId: number; +} + +interface CreateSelfhosted { + provisionUrl: string; + sharedSecret: string; +} + +interface BridgeResponse { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +/** + * Administrative API for configuring Hookshot Webhook bridge instances. + */ +@Path("/api/v1/dimension/admin/hookshot/webhook") +export class AdminHookshotWebhookService { + + @Context + private context: ServiceContext; + + @GET + @Path("all") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridges(): Promise { + const bridges = await HookshotWebhookBridgeRecord.findAll(); + return Promise.all(bridges.map(async b => { + return { + id: b.id, + upstreamId: b.upstreamId, + provisionUrl: b.provisionUrl, + sharedSecret: b.sharedSecret, + isEnabled: b.isEnabled, + }; + })); + } + + @GET + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise { + const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId); + if (!bridge) throw new ApiError(404, "Webhook Bridge not found"); + + return { + id: bridge.id, + upstreamId: bridge.upstreamId, + provisionUrl: bridge.provisionUrl, + sharedSecret: bridge.sharedSecret, + isEnabled: bridge.isEnabled, + }; + } + + @POST + @Path(":bridgeId") + @Security([ROLE_USER, ROLE_ADMIN]) + public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotWebhookBridgeRecord.findByPk(bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + bridge.provisionUrl = request.provisionUrl; + bridge.sharedSecret = request.sharedSecret; + await bridge.save(); + + LogService.info("AdminHookshotWebhookService", userId + " updated Hookshot Webhook Bridge " + bridge.id); + + Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } + + @POST + @Path("new/upstream") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise { + throw new ApiError(400, "Cannot create a webhook bridge from an upstream"); + } + + @POST + @Path("new/selfhosted") + @Security([ROLE_USER, ROLE_ADMIN]) + public async newSelfhosted(request: CreateSelfhosted): Promise { + const userId = this.context.request.user.userId; + + const bridge = await HookshotWebhookBridgeRecord.create({ + provisionUrl: request.provisionUrl, + sharedSecret: request.sharedSecret, + isEnabled: true, + }); + LogService.info("AdminHookshotWebhookService", userId + " created a new Hookshot Webhook Bridge with provisioning URL " + request.provisionUrl); + + Cache.for(CACHE_HOOKSHOT_WEBHOOK_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(bridge.id); + } +} diff --git a/src/api/dimension/DimensionHookshotGithubService.ts b/src/api/dimension/DimensionHookshotGithubService.ts new file mode 100644 index 0000000..00a12c1 --- /dev/null +++ b/src/api/dimension/DimensionHookshotGithubService.ts @@ -0,0 +1,90 @@ +import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; +import { ApiError } from "../ApiError"; +import { LogService } from "matrix-bot-sdk"; +import { ROLE_USER } from "../security/MatrixSecurity"; +import { + HookshotGithubAuthUrls, + HookshotGithubOrgReposDto, + HookshotGithubRoomConfig +} from "../../bridges/models/hookshot"; +import { HookshotGithubBridge } from "../../bridges/HookshotGithubBridge"; + +interface BridgeRoomRequest { + orgId: string; + repoId: string; +} + +/** + * API for interacting with the Hookshot/Github bridge + */ +@Path("/api/v1/dimension/hookshot/github") +export class DimensionHookshotGithubService { + + @Context + private context: ServiceContext; + + @GET + @Path("auth") + @Security(ROLE_USER) + public async getAuthUrls(): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + return await hookshot.getAuthUrls(); + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error getting auth info"); + } + } + + @GET + @Path("locations") + @Security(ROLE_USER) + public async getUserRepos(): Promise<{ locations: HookshotGithubOrgReposDto[] }> { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + const locations = await hookshot.getInstalledLocations(); + return {locations}; + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error getting repo information", "T2B_MISSING_AUTH"); + } + } + + @POST + @Path("room/:roomId/connect") + @Security(ROLE_USER) + public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + return hookshot.bridgeRoom(roomId, request.orgId, request.repoId); + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error bridging room"); + } + } + + @DELETE + @Path("room/:roomId/connections/all") + @Security(ROLE_USER) + public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + const connections = await hookshot.getRoomConfigurations(roomId); + for (const conn of connections) { + await hookshot.unbridgeRoom(roomId, conn.id); + } + return {}; // 200 OK + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error unbridging room"); + } + } +} diff --git a/src/api/dimension/DimensionHookshotJiraService.ts b/src/api/dimension/DimensionHookshotJiraService.ts new file mode 100644 index 0000000..4a9f308 --- /dev/null +++ b/src/api/dimension/DimensionHookshotJiraService.ts @@ -0,0 +1,96 @@ +import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; +import { ApiError } from "../ApiError"; +import { LogService } from "matrix-bot-sdk"; +import { ROLE_USER } from "../security/MatrixSecurity"; +import { HookshotJiraBridge } from "../../bridges/HookshotJiraBridge"; +import { HookshotJiraInstance, HookshotJiraProject, HookshotJiraRoomConfig } from "../../bridges/models/hookshot"; + +interface BridgeRoomRequest { + instanceName: string; + projectKey: string; +} + +/** + * API for interacting with the Hookshot/Jira bridge + */ +@Path("/api/v1/dimension/hookshot/jira") +export class DimensionHookshotJiraService { + + @Context + private context: ServiceContext; + + @GET + @Path("auth") + @Security(ROLE_USER) + public async getAuthUrl(): Promise<{ authUrl: string }> { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + const authUrl = await hookshot.getAuthUrl(); + return {authUrl}; + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error getting auth info"); + } + } + + @GET + @Path("instances") + @Security(ROLE_USER) + public async getInstances(): Promise<{ instances: HookshotJiraInstance[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotJiraBridge(userId); + const userInfo = await hookshot.getLoggedInUserInfo(); + if (!userInfo.loggedIn) { + throw new ApiError(403, "Not logged in", "T2B_NOT_LOGGED_IN"); + } + return {instances: userInfo.instances}; + } + + @GET + @Path("instance/:instanceName/projects") + @Security(ROLE_USER) + public async getProjects(@PathParam("instanceName") instanceName: string): Promise<{ projects: HookshotJiraProject[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotJiraBridge(userId); + const projects = await hookshot.getProjects(instanceName); + return {projects}; + } + + @POST + @Path("room/:roomId/connect") + @Security(ROLE_USER) + public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + return hookshot.bridgeRoom(roomId, request.instanceName, request.projectKey); + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error bridging room"); + } + } + + @DELETE + @Path("room/:roomId/connections/all") + @Security(ROLE_USER) + public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotJiraBridge(userId); + const connections = await hookshot.getRoomConfigurations(roomId); + for (const conn of connections) { + await hookshot.unbridgeRoom(roomId, conn.id); + } + return {}; // 200 OK + } catch (e) { + LogService.error("DimensionHookshotJiraService", e); + throw new ApiError(400, "Error unbridging room"); + } + } +} diff --git a/src/api/dimension/DimensionHookshotWebhookService.ts b/src/api/dimension/DimensionHookshotWebhookService.ts new file mode 100644 index 0000000..c1206b3 --- /dev/null +++ b/src/api/dimension/DimensionHookshotWebhookService.ts @@ -0,0 +1,51 @@ +import { Context, DELETE, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; +import { ApiError } from "../ApiError"; +import { LogService } from "matrix-bot-sdk"; +import { ROLE_USER } from "../security/MatrixSecurity"; +import { HookshotWebhookRoomConfig } from "../../bridges/models/hookshot"; +import { HookshotWebhookBridge } from "../../bridges/HookshotWebhookBridge"; + +interface BridgeRoomRequest { + name?: string; +} + +/** + * API for interacting with the Hookshot/Webhook bridge + */ +@Path("/api/v1/dimension/hookshot/webhook") +export class DimensionHookshotWebhookService { + + @Context + private context: ServiceContext; + + @POST + @Path("room/:roomId/connect") + @Security(ROLE_USER) + public async createWebhook(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotWebhookBridge(userId); + return hookshot.newConnection(roomId, request.name); + } catch (e) { + LogService.error("DimensionHookshotWebhookService", e); + throw new ApiError(400, "Error bridging room"); + } + } + + @DELETE + @Path("room/:roomId/connection/:connectionId/disconnect") + @Security(ROLE_USER) + public async removeWebhook(@PathParam("roomId") roomId: string, @PathParam("connectionId") connectionId: string): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotWebhookBridge(userId); + await hookshot.removeConnection(roomId, connectionId); + return {}; // 200 OK + } catch (e) { + LogService.error("DimensionHookshotWebhookService", e); + throw new ApiError(400, "Error unbridging room"); + } + } +} diff --git a/src/api/dimension/DimensionSlackService.ts b/src/api/dimension/DimensionSlackService.ts index 883c6e5..038db41 100644 --- a/src/api/dimension/DimensionSlackService.ts +++ b/src/api/dimension/DimensionSlackService.ts @@ -52,6 +52,7 @@ export class DimensionSlackService { @DELETE @Path("room/:roomId/link") + @Security(ROLE_USER) public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { const userId = this.context.request.user.userId; diff --git a/src/bridges/HookshotBridge.ts b/src/bridges/HookshotBridge.ts new file mode 100644 index 0000000..e4c43e4 --- /dev/null +++ b/src/bridges/HookshotBridge.ts @@ -0,0 +1,77 @@ +import { LogService } from "matrix-bot-sdk"; +import * as request from "request"; +import { HookshotConnectionsResponse, HookshotConnectionTypeDefinition } from "./models/hookshot"; +import { IHookshotBridgeRecord } from "../db/models/IHookshotBridgeRecord"; + +export abstract class HookshotBridge { + protected constructor(private requestingUserId: string) { + } + + protected abstract getDefaultBridge(): Promise; + + protected async getAllRoomConfigurations(inRoomId: string): Promise { + const bridge = await this.getDefaultBridge(); + + try { + return await this.doProvisionRequest(bridge, "GET", `/v1/${inRoomId}/connections`); + } catch (e) { + if (e.errBody['errcode'] === "HS_NOT_IN_ROOM") { + return []; + } + + throw e; + } + } + + protected async getAllServiceInformation(): Promise { + const bridge = await this.getDefaultBridge(); + const connections = await this.doProvisionRequest(bridge, "GET", `/v1/connectiontypes`); + return Object.values(connections); + } + + protected async doProvisionRequest(bridge: IHookshotBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { + const provisionUrl = bridge.provisionUrl; + const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + LogService.info("HookshotBridge", "Doing provision Hookshot Bridge request: " + url); + + if (!qs) qs = {}; + + if (qs["userId"] === false) delete qs["userId"]; + else if (!qs["userId"]) qs["userId"] = this.requestingUserId; + + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + json: body, + headers: { + "Authorization": `Bearer ${bridge.sharedSecret}`, + }, + }, (err, res, _body) => { + try { + if (err) { + LogService.error("HookshotBridge", "Error calling" + url); + LogService.error("HookshotBridge", err); + reject(err); + } else if (!res) { + LogService.error("HookshotBridge", "There is no response for " + url); + reject(new Error("No response provided - is the service online?")); + } else if (res.statusCode !== 200 && res.statusCode !== 202) { + LogService.error("HookshotBridge", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("HookshotBridge", res.body); + if (typeof (res.body) === "string") res.body = JSON.parse(res.body); + reject({errBody: res.body, error: new Error("Request failed")}); + } else { + if (typeof (res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + } catch (e) { + LogService.error("HookshotBridge", e); + reject(e); + } + }); + }); + } +} diff --git a/src/bridges/HookshotGithubBridge.ts b/src/bridges/HookshotGithubBridge.ts new file mode 100644 index 0000000..000fbfd --- /dev/null +++ b/src/bridges/HookshotGithubBridge.ts @@ -0,0 +1,123 @@ +import HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord"; +import { + HookshotGithubAuthUrls, + HookshotGithubOrg, + HookshotGithubOrgReposDto, + HookshotGithubRepo, + HookshotGithubRoomConfig, + HookshotGithubUserInfo, + HookshotTypes +} from "./models/hookshot"; +import { HookshotBridge } from "./HookshotBridge"; + +export class HookshotGithubBridge extends HookshotBridge { + constructor(requestingUserId: string) { + super(requestingUserId); + } + + protected async getDefaultBridge(): Promise { + const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}}); + if (!bridges || bridges.length !== 1) { + throw new Error("No bridges or too many bridges found"); + } + + return bridges[0]; + } + + public async getAuthUrls(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/github/oauth`).then(r => ({userUrl: r['user_url'], orgUrl: r['org_url']})); + } + + public async getBotUserId(): Promise { + const confs = await this.getAllServiceInformation(); + const conf = confs.find(c => c.eventType === HookshotTypes.Github); + return conf?.botUserId; + } + + public async isBridgingEnabled(): Promise { + const bridges = await HookshotGithubBridgeRecord.findAll({where: {isEnabled: true}}); + return !!bridges && bridges.length > 0 && !!(await this.getBotUserId()); + } + + public async getLoggedInUserInfo(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/github/account`); + } + + public async getRepos(orgId: string): Promise { + const bridge = await this.getDefaultBridge(); + const results: HookshotGithubRepo[] = []; + let more = true; + let page = 1; + let perPage = 10; + do { + const res = await this.doProvisionRequest(bridge, "GET", `/v1/github/orgs/${orgId}/repositories`, { + page, + perPage, + }).then(r => r['repositories']); + results.push(...res); + if (res.length < perPage) more = false; + page++; + } while(more); + return results; + } + + public async getInstalledLocations(): Promise { + const bridge = await this.getDefaultBridge(); + + const orgs: HookshotGithubOrg[] = []; + let lastOrgs: HookshotGithubOrg[] = []; + let page = 1; + let perPage = 10; + do { + const res = await this.doProvisionRequest(bridge, "GET", `/v1/github/account`, {page, perPage}); + lastOrgs = res.organisations; + page++; + orgs.push(...lastOrgs); + } while(lastOrgs.length >= perPage); + + const results: HookshotGithubOrgReposDto[] = []; + for (const org of orgs) { + page = 1; + let lastRepos: HookshotGithubRepo[] = []; + const repos: HookshotGithubRepo[] = []; + let changeUrl: string; + do { + const res = await this.doProvisionRequest(bridge, "GET", `/v1/github/orgs/${org.name}/repositories`, {page, perPage}); + lastRepos = res['repositories']; + changeUrl = res['changeSelectionUrl']; + page++; + repos.push(...lastRepos); + } while(lastRepos.length >= perPage); + + results.push({ + organization: org, + repositories: repos, + changeSelectionUrl: changeUrl, + }); + } + + return results; + } + + public async getRoomConfigurations(inRoomId: string): Promise { + return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Github); + } + + public async bridgeRoom(roomId: string, orgId: string, repoId: string): Promise { + const bridge = await this.getDefaultBridge(); + + const body = { + commandPrefix: "!github", + org: orgId, + repo: repoId, + }; + return await this.doProvisionRequest(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Github}`, null, body); + } + + public async unbridgeRoom(roomId: string, connectionId: string): Promise { + const bridge = await this.getDefaultBridge(); + await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`); + } +} diff --git a/src/bridges/HookshotJiraBridge.ts b/src/bridges/HookshotJiraBridge.ts new file mode 100644 index 0000000..95c393f --- /dev/null +++ b/src/bridges/HookshotJiraBridge.ts @@ -0,0 +1,68 @@ +import HookshotJiraBridgeRecord from "../db/models/HookshotJiraBridgeRecord"; +import { HookshotJiraProject, HookshotJiraRoomConfig, HookshotJiraUserInfo, HookshotTypes } from "./models/hookshot"; +import { HookshotBridge } from "./HookshotBridge"; + +export class HookshotJiraBridge extends HookshotBridge { + constructor(requestingUserId: string) { + super(requestingUserId); + } + + protected async getDefaultBridge(): Promise { + const bridges = await HookshotJiraBridgeRecord.findAll({where: {isEnabled: true}}); + if (!bridges || bridges.length !== 1) { + throw new Error("No bridges or too many bridges found"); + } + + return bridges[0]; + } + + public async getAuthUrl(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/oauth`).then(r => r['url']); + } + + public async getLoggedInUserInfo(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/account`); + } + + public async getProjects(instanceName: string): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/jira/instances/${instanceName}/projects`); + } + + public async getBotUserId(): Promise { + const confs = await this.getAllServiceInformation(); + const conf = confs.find(c => c.eventType === HookshotTypes.Jira); + return conf?.botUserId; + } + + public async isBridgingEnabled(): Promise { + const bridges = await HookshotJiraBridgeRecord.findAll({where: {isEnabled: true}}); + return !!bridges && bridges.length > 0 && !!(await this.getBotUserId()); + } + + public async getRoomConfigurations(inRoomId: string): Promise { + return (await this.getAllRoomConfigurations(inRoomId)) + .filter(c => c.eventType === HookshotTypes.Jira); + } + + public async bridgeRoom(roomId: string, instanceName: string, projectKey: string): Promise { + const bridge = await this.getDefaultBridge(); + + const projects = await this.getProjects(instanceName); + const project = projects.find(p => p.key === projectKey); + if (!project) throw new Error("Could not find project"); + + const body = { + url: project.url, + commandPrefix: "!jira", + }; + return await this.doProvisionRequest(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Jira}`, null, body); + } + + public async unbridgeRoom(roomId: string, connectionId: string): Promise { + const bridge = await this.getDefaultBridge(); + await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${encodeURIComponent(connectionId)}`); + } +} diff --git a/src/bridges/HookshotWebhookBridge.ts b/src/bridges/HookshotWebhookBridge.ts new file mode 100644 index 0000000..b0009b3 --- /dev/null +++ b/src/bridges/HookshotWebhookBridge.ts @@ -0,0 +1,47 @@ +import { HookshotTypes, HookshotWebhookRoomConfig } from "./models/hookshot"; +import { HookshotBridge } from "./HookshotBridge"; +import HookshotWebhookBridgeRecord from "../db/models/HookshotWebhookBridgeRecord"; + +export class HookshotWebhookBridge extends HookshotBridge { + constructor(requestingUserId: string) { + super(requestingUserId); + } + + protected async getDefaultBridge(): Promise { + const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}}); + if (!bridges || bridges.length !== 1) { + throw new Error("No bridges or too many bridges found"); + } + + return bridges[0]; + } + + public async getBotUserId(): Promise { + const confs = await this.getAllServiceInformation(); + const conf = confs.find(c => c.eventType === HookshotTypes.Webhook); + return conf?.botUserId; + } + + public async isBridgingEnabled(): Promise { + const bridges = await HookshotWebhookBridgeRecord.findAll({where: {isEnabled: true}}); + return !!bridges && bridges.length > 0 && !!(await this.getBotUserId()); + } + + public async getRoomConfigurations(inRoomId: string): Promise { + return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Webhook); + } + + public async newConnection(roomId: string, name: string): Promise { + const bridge = await this.getDefaultBridge(); + + const body = { + name, + }; + return await this.doProvisionRequest(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Webhook}`, null, body); + } + + public async removeConnection(roomId: string, connectionId: string): Promise { + const bridge = await this.getDefaultBridge(); + await this.doProvisionRequest(bridge, "DELETE", `/v1/${roomId}/connections/${connectionId}`); + } +} diff --git a/src/bridges/models/hookshot.ts b/src/bridges/models/hookshot.ts new file mode 100644 index 0000000..47c058c --- /dev/null +++ b/src/bridges/models/hookshot.ts @@ -0,0 +1,113 @@ +export enum HookshotTypes { + Github = "uk.half-shot.matrix-hookshot.github.repository", + Jira = "uk.half-shot.matrix-hookshot.jira.project", + Webhook = "uk.half-shot.matrix-hookshot.generic.hook", +} + +export interface HookshotConnection { + type: string; + eventType: string; // state key in the connection + id: string; + service: string; // human-readable + botUserId: string; + config: any; // context-specific +} + +export type HookshotConnectionsResponse = HookshotConnection[]; + +export interface HookshotConnectionTypeDefinition { + type: string; // name of connection + eventType: string; // state key in the connection + service: string; // human-readable + botUserId: string; +} + +export interface HookshotGithubRoomConfig extends HookshotConnection { + config: { + org: string; + repo: string; + ignoreHooks: SupportedGithubRepoEventType[]; + commandPrefix: string; + }; +} + +export interface HookshotGithubOrg { + name: string; + avatarUrl: string; +} + +export interface HookshotGithubOrgReposResponse { + repositories: HookshotGithubRepo[]; + changeSelectionUrl?: string; +} + +export interface HookshotGithubRepo { + name: string; + owner: string; + fullName: string; + avatarUrl: string; + description: string; +} + +export interface HookshotGithubOrgReposDto { + organization: HookshotGithubOrg; + repositories: HookshotGithubRepo[]; + changeSelectionUrl?: string; +} + +export interface HookshotGithubUserInfo { + loggedIn: boolean; + organisations?: HookshotGithubOrg[]; +} + +export interface HookshotGithubAuthUrls { + userUrl: string; + orgUrl: string; +} + +export enum SupportedGithubRepoEventType { + IssueCreated = "issue.created", + IssueChanged = "issue.changed", + IssueEdited = "issue.edited", + Issue = "issue", + PROpened = "pull_request.opened", + PRClosed = "pull_request.closed", + PRMerged = "pull_request.merged", + PRReadyForReview = "pull_request.ready_for_review", + PRReviewed = "pull_request.reviewed", + PR = "pull_request", + ReleaseCreated = "release.created", + Release = "release", +} + +export interface HookshotJiraRoomConfig extends HookshotConnection { + config: { + url: string; + events: SupportedJiraEventType[]; + commandPrefix: string; + }; +} + +export enum SupportedJiraEventType { + IssueCreated = "issue.created", +} + +export interface HookshotJiraUserInfo { + loggedIn: boolean; + instances?: HookshotJiraInstance[]; +} + +export interface HookshotJiraInstance { + name: string; + url: string; +} + +export interface HookshotJiraProject { + key: string; + name: string; + url: string; +} + +export interface HookshotWebhookRoomConfig extends HookshotConnection { + config: {}; +} diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index 64a47a7..806e242 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -1,5 +1,8 @@ import { Bridge, + HookshotGithubBridgeConfiguration, + HookshotJiraBridgeConfiguration, + HookshotWebhookBridgeConfiguration, SlackBridgeConfiguration, TelegramBridgeConfiguration, WebhookBridgeConfiguration @@ -10,6 +13,9 @@ import { LogService } from "matrix-bot-sdk"; import { TelegramBridge } from "../bridges/TelegramBridge"; import { WebhooksBridge } from "../bridges/WebhooksBridge"; import { SlackBridge } from "../bridges/SlackBridge"; +import { HookshotGithubBridge } from "../bridges/HookshotGithubBridge"; +import { HookshotJiraBridge } from "../bridges/HookshotJiraBridge"; +import { HookshotWebhookBridge } from "../bridges/HookshotWebhookBridge"; export class BridgeStore { @@ -59,7 +65,7 @@ export class BridgeStore { const record = await BridgeRecord.findOne({where: {type: integrationType}}); if (!record) throw new Error("Bridge not found"); - const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack"]; + const hasDedicatedApi = ["irc", "telegram", "webhooks", "slack", "hookshot_github", "hookshot_jira"]; if (hasDedicatedApi.indexOf(integrationType) !== -1) { throw new Error("This bridge should be modified with the dedicated API"); } else throw new Error("Unsupported bridge"); @@ -78,6 +84,15 @@ export class BridgeStore { } else if (record.type === "slack") { const slack = new SlackBridge(requestingUserId); return slack.isBridgingEnabled(); + } else if (record.type === "hookshot_github") { + const hookshot = new HookshotGithubBridge(requestingUserId); + return hookshot.isBridgingEnabled(); + } else if (record.type === "hookshot_jira") { + const hookshot = new HookshotJiraBridge(requestingUserId); + return hookshot.isBridgingEnabled(); + } else if (record.type === "hookshot_webhook") { + const hookshot = new HookshotWebhookBridge(requestingUserId); + return hookshot.isBridgingEnabled(); } else return true; } @@ -94,6 +109,15 @@ export class BridgeStore { } else if (record.type === "slack") { const slack = new SlackBridge(requestingUserId); return slack.isBridgingEnabled(); + } else if (record.type === "hookshot_github") { + const hookshot = new HookshotGithubBridge(requestingUserId); + return hookshot.isBridgingEnabled(); + } else if (record.type === "hookshot_jira") { + const hookshot = new HookshotJiraBridge(requestingUserId); + return hookshot.isBridgingEnabled(); + } else if (record.type === "hookshot_webhook") { + const hookshot = new HookshotWebhookBridge(requestingUserId); + return hookshot.isBridgingEnabled(); } else return false; } @@ -131,6 +155,36 @@ export class BridgeStore { link: link, botUserId: info.botUserId, }; + } else if (record.type === "hookshot_github") { + if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs + const hookshot = new HookshotGithubBridge(requestingUserId); + const botUserId = await hookshot.getBotUserId(); + const connections = await hookshot.getRoomConfigurations(inRoomId); + return { + botUserId: botUserId, + connections: connections, + }; + } else if (record.type === "hookshot_jira") { + if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs + const hookshot = new HookshotJiraBridge(requestingUserId); + const botUserId = await hookshot.getBotUserId(); + const connections = await hookshot.getRoomConfigurations(inRoomId); + const userInfo = await hookshot.getLoggedInUserInfo(); + return { + botUserId: botUserId, + connections: connections, + loggedIn: userInfo.loggedIn, + instances: userInfo.instances, + }; + } else if (record.type === "hookshot_webhook") { + if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs + const hookshot = new HookshotWebhookBridge(requestingUserId); + const botUserId = await hookshot.getBotUserId(); + const connections = await hookshot.getRoomConfigurations(inRoomId); + return { + botUserId: botUserId, + connections: connections, + }; } else return {}; } diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index 7f171fc..64294f1 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -29,6 +29,9 @@ import TermsRecord from "./models/TermsRecord"; import TermsTextRecord from "./models/TermsTextRecord"; import TermsSignedRecord from "./models/TermsSignedRecord"; import TermsUpstreamRecord from "./models/TermsUpstreamRecord"; +import HookshotGithubBridgeRecord from "./models/HookshotGithubBridgeRecord"; +import HookshotJiraBridgeRecord from "./models/HookshotJiraBridgeRecord"; +import HookshotWebhookBridgeRecord from "./models/HookshotWebhookBridgeRecord"; class _DimensionStore { private sequelize: Sequelize; @@ -75,6 +78,9 @@ class _DimensionStore { TermsTextRecord, TermsSignedRecord, TermsUpstreamRecord, + HookshotGithubBridgeRecord, + HookshotJiraBridgeRecord, + HookshotWebhookBridgeRecord, ]); } diff --git a/src/db/migrations/20211124135245-RemoveGitterRecord.ts b/src/db/migrations/20211124135245-RemoveGitterRecord.ts index d8d84c7..eef9912 100644 --- a/src/db/migrations/20211124135245-RemoveGitterRecord.ts +++ b/src/db/migrations/20211124135245-RemoveGitterRecord.ts @@ -13,7 +13,7 @@ export default { { type: "gitter", name: "Gitter Bridge", - avatarUrl: "/img/avatars/gitter.png", + avatarUrl: "/assets/img/avatars/gitter.png", isEnabled: true, isPublic: true, description: "Bridges Gitter rooms to Matrix", diff --git a/src/db/migrations/20211125150045-AddHookshotGithubBridge.ts b/src/db/migrations/20211125150045-AddHookshotGithubBridge.ts new file mode 100644 index 0000000..81e91ef --- /dev/null +++ b/src/db/migrations/20211125150045-AddHookshotGithubBridge.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_hookshot_github_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "upstreamId": { + type: DataType.INTEGER, allowNull: true, + references: {model: "dimension_upstreams", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "provisionUrl": {type: DataType.STRING, allowNull: true}, + "sharedSecret": {type: DataType.STRING, allowNull: true}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_hookshot_github_bridges")); + } +} diff --git a/src/db/migrations/20211125150145-AddHookshotGithubBridgeRecord.ts b/src/db/migrations/20211125150145-AddHookshotGithubBridgeRecord.ts new file mode 100644 index 0000000..be8d31d --- /dev/null +++ b/src/db/migrations/20211125150145-AddHookshotGithubBridgeRecord.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_bridges", [ + { + type: "hookshot_github", + name: "Github Bridge", + avatarUrl: "/assets/img/avatars/github.png", + isEnabled: true, + isPublic: true, + description: "Bridges Github issues to Matrix", + }, + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_bridges", { + type: "hookshot_github", + })); + } +} diff --git a/src/db/migrations/20211130153845-AddHookshotJiraBridge.ts b/src/db/migrations/20211130153845-AddHookshotJiraBridge.ts new file mode 100644 index 0000000..3fbd5fa --- /dev/null +++ b/src/db/migrations/20211130153845-AddHookshotJiraBridge.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_hookshot_jira_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "upstreamId": { + type: DataType.INTEGER, allowNull: true, + references: {model: "dimension_upstreams", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "provisionUrl": {type: DataType.STRING, allowNull: true}, + "sharedSecret": {type: DataType.STRING, allowNull: true}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_hookshot_jira_bridges")); + } +} diff --git a/src/db/migrations/20211130153945-AddHookshotJiraBridgeRecord.ts b/src/db/migrations/20211130153945-AddHookshotJiraBridgeRecord.ts new file mode 100644 index 0000000..df6f16e --- /dev/null +++ b/src/db/migrations/20211130153945-AddHookshotJiraBridgeRecord.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_bridges", [ + { + type: "hookshot_jira", + name: "Jira Bridge", + avatarUrl: "/assets/img/avatars/jira.png", + isEnabled: true, + isPublic: true, + description: "Bridges Jira issues to Matrix", + }, + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_bridges", { + type: "hookshot_jira", + })); + } +} diff --git a/src/db/migrations/20211202181645-AddHookshotWebhookBridge.ts b/src/db/migrations/20211202181645-AddHookshotWebhookBridge.ts new file mode 100644 index 0000000..5bf2300 --- /dev/null +++ b/src/db/migrations/20211202181645-AddHookshotWebhookBridge.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_hookshot_webhook_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "upstreamId": { + type: DataType.INTEGER, allowNull: true, + references: {model: "dimension_upstreams", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "provisionUrl": {type: DataType.STRING, allowNull: true}, + "sharedSecret": {type: DataType.STRING, allowNull: true}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_hookshot_webhook_bridges")); + } +} diff --git a/src/db/migrations/20211202181745-AddHookshotWebhookBridgeRecord.ts b/src/db/migrations/20211202181745-AddHookshotWebhookBridgeRecord.ts new file mode 100644 index 0000000..e41cbf0 --- /dev/null +++ b/src/db/migrations/20211202181745-AddHookshotWebhookBridgeRecord.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_bridges", [ + { + type: "hookshot_webhook", + name: "Webhooks Bridge", + avatarUrl: "/assets/img/avatars/webhooks.png", + isEnabled: true, + isPublic: true, + description: "Webhooks to Matrix", + }, + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_bridges", { + type: "hookshot_webhook", + })); + } +} diff --git a/src/db/migrations/20211202183445-FixLegacyWebhooksBridgeName.ts b/src/db/migrations/20211202183445-FixLegacyWebhooksBridgeName.ts new file mode 100644 index 0000000..5c5af72 --- /dev/null +++ b/src/db/migrations/20211202183445-FixLegacyWebhooksBridgeName.ts @@ -0,0 +1,16 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkUpdate("dimension_bridges", { + name: "Webhooks Bridge", + }, { type: "webhooks" })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkUpdate("dimension_bridges", { + name: "Webhook Bridge", + }, { type: "webhooks" })); + } +} diff --git a/src/db/models/HookshotGithubBridgeRecord.ts b/src/db/models/HookshotGithubBridgeRecord.ts new file mode 100644 index 0000000..9966ae6 --- /dev/null +++ b/src/db/models/HookshotGithubBridgeRecord.ts @@ -0,0 +1,31 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import Upstream from "./Upstream"; +import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord"; + +@Table({ + tableName: "dimension_hookshot_github_bridges", + underscored: false, + timestamps: false, +}) +export default class HookshotGithubBridgeRecord extends Model implements IHookshotBridgeRecord { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => Upstream) + upstreamId?: number; + + @AllowNull + @Column + provisionUrl?: string; + + @AllowNull + @Column + sharedSecret?: string; + + @Column + isEnabled: boolean; +} diff --git a/src/db/models/HookshotJiraBridgeRecord.ts b/src/db/models/HookshotJiraBridgeRecord.ts new file mode 100644 index 0000000..01a40fa --- /dev/null +++ b/src/db/models/HookshotJiraBridgeRecord.ts @@ -0,0 +1,31 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import Upstream from "./Upstream"; +import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord"; + +@Table({ + tableName: "dimension_hookshot_jira_bridges", + underscored: false, + timestamps: false, +}) +export default class HookshotJiraBridgeRecord extends Model implements IHookshotBridgeRecord { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => Upstream) + upstreamId?: number; + + @AllowNull + @Column + provisionUrl?: string; + + @AllowNull + @Column + sharedSecret?: string; + + @Column + isEnabled: boolean; +} diff --git a/src/db/models/HookshotWebhookBridgeRecord.ts b/src/db/models/HookshotWebhookBridgeRecord.ts new file mode 100644 index 0000000..8dc898f --- /dev/null +++ b/src/db/models/HookshotWebhookBridgeRecord.ts @@ -0,0 +1,31 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import Upstream from "./Upstream"; +import { IHookshotBridgeRecord } from "./IHookshotBridgeRecord"; + +@Table({ + tableName: "dimension_hookshot_webhook_bridges", + underscored: false, + timestamps: false, +}) +export default class HookshotWebhookBridgeRecord extends Model implements IHookshotBridgeRecord { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => Upstream) + upstreamId?: number; + + @AllowNull + @Column + provisionUrl?: string; + + @AllowNull + @Column + sharedSecret?: string; + + @Column + isEnabled: boolean; +} diff --git a/src/db/models/IHookshotBridgeRecord.ts b/src/db/models/IHookshotBridgeRecord.ts new file mode 100644 index 0000000..73db840 --- /dev/null +++ b/src/db/models/IHookshotBridgeRecord.ts @@ -0,0 +1,6 @@ +export interface IHookshotBridgeRecord { + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} diff --git a/src/index.ts b/src/index.ts index d29bae3..7e445cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { LogLevel, LogService } from "matrix-bot-sdk"; +import { LogLevel, LogService, RichConsoleLogger } from "matrix-bot-sdk"; import { DimensionStore } from "./db/DimensionStore"; import Webserver from "./api/Webserver"; import { CURRENT_VERSION } from "./version"; @@ -16,11 +16,12 @@ declare global { } LogService.setLevel(LogLevel.DEBUG); +LogService.setLogger(new RichConsoleLogger()); LogService.info("index", "Starting dimension " + CURRENT_VERSION); async function startup() { const schemas = await DimensionStore.updateSchema(); - LogService.info("DimensionStore", schemas); + LogService.info("DimensionStore", "Applied schemas: ", schemas); const webserver = new Webserver(); await webserver.start(); diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index 9de882f..624bc87 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -4,6 +4,12 @@ import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge"; import { PortalInfo, PuppetInfo } from "../bridges/TelegramBridge"; import { WebhookConfiguration } from "../bridges/models/webhooks"; import { BridgedChannel } from "../bridges/SlackBridge"; +import { + HookshotGithubRoomConfig, + HookshotJiraInstance, + HookshotJiraRoomConfig, + HookshotWebhookRoomConfig +} from "../bridges/models/hookshot"; const PRIVATE_ACCESS_SUPPORTED_BRIDGES = ["webhooks"]; @@ -45,3 +51,20 @@ export interface SlackBridgeConfiguration { link: BridgedChannel; botUserId: string; } + +export interface HookshotGithubBridgeConfiguration { + botUserId: string; + connections: HookshotGithubRoomConfig[]; +} + +export interface HookshotJiraBridgeConfiguration { + botUserId: string; + connections: HookshotJiraRoomConfig[]; + loggedIn: boolean; + instances?: HookshotJiraInstance[]; +} + +export interface HookshotWebhookBridgeConfiguration { + botUserId: string; + connections: HookshotWebhookRoomConfig[]; +} diff --git a/web/app/admin/admin.component.scss b/web/app/admin/admin.component.scss index 5124dc2..e383b16 100644 --- a/web/app/admin/admin.component.scss +++ b/web/app/admin/admin.component.scss @@ -2,8 +2,8 @@ @include themifyComponent() { .adminNav { + margin: 32px 0 0; width: 200px; - margin: 0; padding: 0; position: fixed; top: 100px; @@ -34,8 +34,9 @@ } .adminContent { + margin-top: 32px; position: fixed; - top: 120px; + top: 85px; left: 200px; bottom: 0; right: 0; @@ -53,4 +54,4 @@ font-size: 11px; font-family: monospace; } -} \ No newline at end of file +} diff --git a/web/app/admin/bridges/hookshot-github/hookshot-github.component.html b/web/app/admin/bridges/hookshot-github/hookshot-github.component.html new file mode 100644 index 0000000..78fa9c4 --- /dev/null +++ b/web/app/admin/bridges/hookshot-github/hookshot-github.component.html @@ -0,0 +1,41 @@ +
+ +
+
+ +
+

+ {{'matrix-hookshot' | translate}} + {{'is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.' | translate}} + + + + + + + + + + + + + + + + +
{{'Name' | translate}}{{'Actions' | translate}}
{{'No bridge configurations.' | translate}}
+ {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} + ({{ bridge.provisionUrl }}) + + + + +
+ +

+
+
diff --git a/web/app/admin/bridges/hookshot-github/hookshot-github.component.scss b/web/app/admin/bridges/hookshot-github/hookshot-github.component.scss new file mode 100644 index 0000000..788d7ed --- /dev/null +++ b/web/app/admin/bridges/hookshot-github/hookshot-github.component.scss @@ -0,0 +1,3 @@ +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/hookshot-github/hookshot-github.component.ts b/web/app/admin/bridges/hookshot-github/hookshot-github.component.ts new file mode 100644 index 0000000..08fd528 --- /dev/null +++ b/web/app/admin/bridges/hookshot-github/hookshot-github.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { + AdminHookshotGithubBridgeManageSelfhostedComponent, + ManageSelfhostedHookshotGithubBridgeDialogContext +} from "./manage-selfhosted/manage-selfhosted.component"; +import { TranslateService } from "@ngx-translate/core"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { AdminHookshotGithubApiService } from "../../../shared/services/admin/admin-hookshot-github-api.service"; +import { FE_HookshotGithubBridge } from "../../../shared/models/hookshot_github"; + +@Component({ + templateUrl: "./hookshot-github.component.html", + styleUrls: ["./hookshot-github.component.scss"], +}) +export class AdminHookshotGithubBridgeComponent implements OnInit { + + public isLoading = true; + public isUpdating = false; + public configurations: FE_HookshotGithubBridge[] = []; + + constructor(private hookshotApi: AdminHookshotGithubApiService, + private toaster: ToasterService, + private modal: NgbModal, + public translate: TranslateService) { + this.translate = translate; + } + + public ngOnInit() { + this.reload().then(() => this.isLoading = false); + } + + private async reload(): Promise { + try { + this.configurations = await this.hookshotApi.getBridges(); + } catch (err) { + console.error(err); + this.translate.get('Error loading bridges').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + } + + public addSelfHostedBridge() { + const selfhostedRef = this.modal.open(AdminHookshotGithubBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Github bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotGithubBridgeDialogContext; + selfhostedInstance.provisionUrl = ''; + selfhostedInstance.sharedSecret = ''; + } + + public editBridge(bridge: FE_HookshotGithubBridge) { + const selfhostedRef = this.modal.open(AdminHookshotGithubBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Github bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotGithubBridgeDialogContext; + selfhostedInstance.provisionUrl = bridge.provisionUrl; + selfhostedInstance.sharedSecret = bridge.sharedSecret; + selfhostedInstance.bridgeId = bridge.id; + selfhostedInstance.isAdding = !bridge.id; + } +} diff --git a/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.html b/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.html new file mode 100644 index 0000000..b814377 --- /dev/null +++ b/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.html @@ -0,0 +1,31 @@ + + + diff --git a/web/app/configs/simple-bot/simple-bot.component.scss b/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.scss similarity index 100% rename from web/app/configs/simple-bot/simple-bot.component.scss rename to web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.scss diff --git a/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.ts b/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.ts new file mode 100644 index 0000000..b458bd8 --- /dev/null +++ b/web/app/admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component.ts @@ -0,0 +1,63 @@ +import { Component } from "@angular/core"; +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; +import { ToasterService } from "angular2-toaster"; +import { TranslateService } from "@ngx-translate/core"; +import { AdminHookshotGithubApiService } from "../../../../shared/services/admin/admin-hookshot-github-api.service"; + +export interface ManageSelfhostedHookshotGithubBridgeDialogContext { + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding: boolean; +} + +@Component({ + templateUrl: "./manage-selfhosted.component.html", + styleUrls: ["./manage-selfhosted.component.scss"], +}) +export class AdminHookshotGithubBridgeManageSelfhostedComponent { + + isSaving = false; + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding = true; + + constructor(public modal: NgbActiveModal, + private hookshotApi: AdminHookshotGithubApiService, + private toaster: ToasterService, + public translate: TranslateService) { + this.translate = translate; + } + + public add() { + this.isSaving = true; + if (this.isAdding) { + this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Github bridge added').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to create Github bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } else { + this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Github bridge updated').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to update Github bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + } +} diff --git a/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.html b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.html new file mode 100644 index 0000000..81ecc03 --- /dev/null +++ b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.html @@ -0,0 +1,41 @@ +
+ +
+
+ +
+

+ {{'matrix-hookshot' | translate}} + {{'is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.' | translate}} + + + + + + + + + + + + + + + + +
{{'Name' | translate}}{{'Actions' | translate}}
{{'No bridge configurations.' | translate}}
+ {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} + ({{ bridge.provisionUrl }}) + + + + +
+ +

+
+
diff --git a/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.scss b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.scss new file mode 100644 index 0000000..788d7ed --- /dev/null +++ b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.scss @@ -0,0 +1,3 @@ +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.ts b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.ts new file mode 100644 index 0000000..eb364da --- /dev/null +++ b/web/app/admin/bridges/hookshot-jira/hookshot-jira.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { + AdminHookshotJiraBridgeManageSelfhostedComponent, + ManageSelfhostedHookshotJiraBridgeDialogContext +} from "./manage-selfhosted/manage-selfhosted.component"; +import { TranslateService } from "@ngx-translate/core"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { FE_HookshotJiraBridge } from "../../../shared/models/hookshot_jira"; +import { AdminHookshotJiraApiService } from "../../../shared/services/admin/admin-hookshot-jira-api.service"; + +@Component({ + templateUrl: "./hookshot-jira.component.html", + styleUrls: ["./hookshot-jira.component.scss"], +}) +export class AdminHookshotJiraBridgeComponent implements OnInit { + + public isLoading = true; + public isUpdating = false; + public configurations: FE_HookshotJiraBridge[] = []; + + constructor(private hookshotApi: AdminHookshotJiraApiService, + private toaster: ToasterService, + private modal: NgbModal, + public translate: TranslateService) { + this.translate = translate; + } + + public ngOnInit() { + this.reload().then(() => this.isLoading = false); + } + + private async reload(): Promise { + try { + this.configurations = await this.hookshotApi.getBridges(); + } catch (err) { + console.error(err); + this.translate.get('Error loading bridges').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + } + + public addSelfHostedBridge() { + const selfhostedRef = this.modal.open(AdminHookshotJiraBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext; + selfhostedInstance.provisionUrl = ''; + selfhostedInstance.sharedSecret = ''; + } + + public editBridge(bridge: FE_HookshotJiraBridge) { + const selfhostedRef = this.modal.open(AdminHookshotJiraBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Jira bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotJiraBridgeDialogContext; + selfhostedInstance.provisionUrl = bridge.provisionUrl; + selfhostedInstance.sharedSecret = bridge.sharedSecret; + selfhostedInstance.bridgeId = bridge.id; + selfhostedInstance.isAdding = !bridge.id; + } +} diff --git a/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.html b/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.html new file mode 100644 index 0000000..cbdcfd3 --- /dev/null +++ b/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.html @@ -0,0 +1,31 @@ + + + diff --git a/web/app/riot/scalar-close/scalar-close.component.scss b/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.scss similarity index 100% rename from web/app/riot/scalar-close/scalar-close.component.scss rename to web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.scss diff --git a/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.ts b/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.ts new file mode 100644 index 0000000..98eaa66 --- /dev/null +++ b/web/app/admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component.ts @@ -0,0 +1,63 @@ +import { Component } from "@angular/core"; +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; +import { ToasterService } from "angular2-toaster"; +import { TranslateService } from "@ngx-translate/core"; +import { AdminHookshotJiraApiService } from "../../../../shared/services/admin/admin-hookshot-jira-api.service"; + +export interface ManageSelfhostedHookshotJiraBridgeDialogContext { + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding: boolean; +} + +@Component({ + templateUrl: "./manage-selfhosted.component.html", + styleUrls: ["./manage-selfhosted.component.scss"], +}) +export class AdminHookshotJiraBridgeManageSelfhostedComponent { + + isSaving = false; + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding = true; + + constructor(public modal: NgbActiveModal, + private hookshotApi: AdminHookshotJiraApiService, + private toaster: ToasterService, + public translate: TranslateService) { + this.translate = translate; + } + + public add() { + this.isSaving = true; + if (this.isAdding) { + this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Jira bridge added').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to create Jira bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } else { + this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Jira bridge updated').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to update Jira bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + } +} diff --git a/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.html b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.html new file mode 100644 index 0000000..beb8b20 --- /dev/null +++ b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.html @@ -0,0 +1,41 @@ +
+ +
+
+ +
+

+ {{'matrix-hookshot' | translate}} + {{'is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.' | translate}} + + + + + + + + + + + + + + + + +
{{'Name' | translate}}{{'Actions' | translate}}
{{'No bridge configurations.' | translate}}
+ {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} + ({{ bridge.provisionUrl }}) + + + + +
+ +

+
+
diff --git a/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.scss b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.scss new file mode 100644 index 0000000..788d7ed --- /dev/null +++ b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.scss @@ -0,0 +1,3 @@ +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.ts b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.ts new file mode 100644 index 0000000..6e8e11a --- /dev/null +++ b/web/app/admin/bridges/hookshot-webhook/hookshot-webhook.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { + AdminHookshotWebhookBridgeManageSelfhostedComponent, + ManageSelfhostedHookshotWebhookBridgeDialogContext +} from "./manage-selfhosted/manage-selfhosted.component"; +import { TranslateService } from "@ngx-translate/core"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { AdminHookshotWebhookApiService } from "../../../shared/services/admin/admin-hookshot-webhook-api.service"; +import { FE_HookshotWebhookBridge } from "../../../shared/models/hookshot_webhook"; + +@Component({ + templateUrl: "./hookshot-webhook.component.html", + styleUrls: ["./hookshot-webhook.component.scss"], +}) +export class AdminHookshotWebhookBridgeComponent implements OnInit { + + public isLoading = true; + public isUpdating = false; + public configurations: FE_HookshotWebhookBridge[] = []; + + constructor(private hookshotApi: AdminHookshotWebhookApiService, + private toaster: ToasterService, + private modal: NgbModal, + public translate: TranslateService) { + this.translate = translate; + } + + public ngOnInit() { + this.reload().then(() => this.isLoading = false); + } + + private async reload(): Promise { + try { + this.configurations = await this.hookshotApi.getBridges(); + } catch (err) { + console.error(err); + this.translate.get('Error loading bridges').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + } + + public addSelfHostedBridge() { + const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext; + selfhostedInstance.provisionUrl = ''; + selfhostedInstance.sharedSecret = ''; + } + + public editBridge(bridge: FE_HookshotWebhookBridge) { + const selfhostedRef = this.modal.open(AdminHookshotWebhookBridgeManageSelfhostedComponent, { + backdrop: 'static', + size: 'lg', + }); + selfhostedRef.result.then(() => { + try { + this.reload() + } catch (err) { + console.error(err); + this.translate.get('Failed to get an updated Webhook bridge list').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }) + const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedHookshotWebhookBridgeDialogContext; + selfhostedInstance.provisionUrl = bridge.provisionUrl; + selfhostedInstance.sharedSecret = bridge.sharedSecret; + selfhostedInstance.bridgeId = bridge.id; + selfhostedInstance.isAdding = !bridge.id; + } +} diff --git a/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.html b/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.html new file mode 100644 index 0000000..767cf02 --- /dev/null +++ b/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.html @@ -0,0 +1,31 @@ + + + diff --git a/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.scss b/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.ts b/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.ts new file mode 100644 index 0000000..8902f67 --- /dev/null +++ b/web/app/admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component.ts @@ -0,0 +1,63 @@ +import { Component } from "@angular/core"; +import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; +import { ToasterService } from "angular2-toaster"; +import { TranslateService } from "@ngx-translate/core"; +import { AdminHookshotWebhookApiService } from "../../../../shared/services/admin/admin-hookshot-webhook-api.service"; + +export interface ManageSelfhostedHookshotWebhookBridgeDialogContext { + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding: boolean; +} + +@Component({ + templateUrl: "./manage-selfhosted.component.html", + styleUrls: ["./manage-selfhosted.component.scss"], +}) +export class AdminHookshotWebhookBridgeManageSelfhostedComponent { + + isSaving = false; + provisionUrl: string; + sharedSecret: string; + bridgeId: number; + isAdding = true; + + constructor(public modal: NgbActiveModal, + private hookshotApi: AdminHookshotWebhookApiService, + private toaster: ToasterService, + public translate: TranslateService) { + this.translate = translate; + } + + public add() { + this.isSaving = true; + if (this.isAdding) { + this.hookshotApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Webhook bridge added').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to create Webhook bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } else { + this.hookshotApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => { + this.translate.get('Webhook bridge updated').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + this.modal.close(); + }).catch(err => { + console.error(err); + this.isSaving = false; + this.translate.get('Failed to update Webhook bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + } +} diff --git a/web/app/admin/bridges/irc/irc.component.ts b/web/app/admin/bridges/irc/irc.component.ts index fee6cc4..56040cc 100644 --- a/web/app/admin/bridges/irc/irc.component.ts +++ b/web/app/admin/bridges/irc/irc.component.ts @@ -5,9 +5,7 @@ import { FE_Upstream } from "../../../shared/models/admin-responses"; import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service"; import { FE_IrcBridge } from "../../../shared/models/irc"; import { AdminIrcBridgeNetworksComponent, IrcNetworksDialogContext } from "./networks/networks.component"; -import { - AdminIrcBridgeAddSelfhostedComponent -} from "./add-selfhosted/add-selfhosted.component"; +import { AdminIrcBridgeAddSelfhostedComponent } from "./add-selfhosted/add-selfhosted.component"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; diff --git a/web/app/admin/custom-bots/custom-bots.component.scss b/web/app/admin/custom-bots/custom-bots.component.scss index 2fdecca..d6698cf 100644 --- a/web/app/admin/custom-bots/custom-bots.component.scss +++ b/web/app/admin/custom-bots/custom-bots.component.scss @@ -4,5 +4,5 @@ tr td:last-child { .editButton { cursor: pointer; - vertical-align: text-bottom; -} \ No newline at end of file + vertical-align: middle; +} diff --git a/web/app/admin/home/home.component.ts b/web/app/admin/home/home.component.ts index 69c4913..f118db5 100644 --- a/web/app/admin/home/home.component.ts +++ b/web/app/admin/home/home.component.ts @@ -2,9 +2,7 @@ import { Component } from "@angular/core"; import { AdminApiService } from "../../shared/services/admin/admin-api.service"; import { FE_DimensionConfig } from "../../shared/models/admin-responses"; import { ToasterService } from "angular2-toaster"; -import { - AdminLogoutConfirmationDialogComponent, -} from "./logout-confirmation/logout-confirmation.component"; +import { AdminLogoutConfirmationDialogComponent, } from "./logout-confirmation/logout-confirmation.component"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; diff --git a/web/app/admin/sticker-packs/sticker-packs.component.scss b/web/app/admin/sticker-packs/sticker-packs.component.scss index 9e19650..054e069 100644 --- a/web/app/admin/sticker-packs/sticker-packs.component.scss +++ b/web/app/admin/sticker-packs/sticker-packs.component.scss @@ -4,12 +4,12 @@ tr td:last-child { .previewButton { cursor: pointer; - vertical-align: text-bottom; + vertical-align: middle; } .removeButton { cursor: pointer; - vertical-align: text-bottom; + vertical-align: middle; } .telegram-import { diff --git a/web/app/admin/terms/new-edit/new-edit.component.ts b/web/app/admin/terms/new-edit/new-edit.component.ts index c8ae758..fc17b0e 100644 --- a/web/app/admin/terms/new-edit/new-edit.component.ts +++ b/web/app/admin/terms/new-edit/new-edit.component.ts @@ -4,9 +4,7 @@ import { AdminTermsApiService } from "../../../shared/services/admin/admin-terms import { ActivatedRoute, Router } from "@angular/router"; import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import ISO6391 from "iso-639-1"; -import { - AdminTermsNewEditPublishDialogComponent, -} from "./publish/publish.component"; +import { AdminTermsNewEditPublishDialogComponent, } from "./publish/publish.component"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; diff --git a/web/app/app.component.scss b/web/app/app.component.scss index b6c784b..44ef9f0 100644 --- a/web/app/app.component.scss +++ b/web/app/app.component.scss @@ -4,6 +4,6 @@ } main { - font-family: 'Roboto', 'Open Sans', Arial, sans-serif; + font-family: 'Inter', Arial, sans-serif; display: block; } diff --git a/web/app/app.component.ts b/web/app/app.component.ts index 7f34b96..7a37522 100644 --- a/web/app/app.component.ts +++ b/web/app/app.component.ts @@ -11,7 +11,7 @@ import { HttpClient } from "@angular/common/http"; export class AppComponent { constructor(public translate: TranslateService, public http: HttpClient) { translate.addLangs(["en", "de"]); - translate.setDefaultLang("de"); + translate.setDefaultLang("en"); if (navigator.language === "de") { translate.use("de"); } else { diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 5e37ff9..ba2890e 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -10,10 +10,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { AppComponent } from "./app.component"; import { HomeComponent } from "./home/home.component"; import { createNewHosts, removeNgStyles } from "@angularclass/hmr"; -import { RiotComponent } from "./riot/riot.component"; +import { ElementComponent } from "./element/element.component"; import { ScalarClientApiService } from "./shared/services/scalar/scalar-client-api.service"; import { ToasterModule, ToasterService } from "angular2-toaster"; -import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component"; +import { ScalarCloseComponent } from "./element/scalar-close/scalar-close.component"; import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component"; import { ToggleFullscreenDirective } from "./shared/directives/toggle-fullscreen.directive"; import { FullscreenButtonComponent } from "./elements/fullscreen-button/fullscreen-button.component"; @@ -22,7 +22,7 @@ import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.compo import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component"; import { PageHeaderComponent } from "./page-header/page-header.component"; import { SpinnerComponent } from "./elements/spinner/spinner.component"; -import { RiotHomeComponent } from "./riot/riot-home/home.component"; +import { ElementHomeComponent } from "./element/element-home/home.component"; import { IntegrationBagComponent } from "./integration-bag/integration-bag.component"; import { ScalarServerApiService } from "./shared/services/scalar/scalar-server-api.service"; import { AdminApiService } from "./shared/services/admin/admin-api.service"; @@ -56,8 +56,9 @@ import { AdminNebGiphyConfigComponent } from "./admin/neb/config/giphy/giphy.com import { AdminNebGuggyConfigComponent } from "./admin/neb/config/guggy/guggy.component"; import { AdminNebGoogleConfigComponent } from "./admin/neb/config/google/google.component"; import { AdminNebImgurConfigComponent } from "./admin/neb/config/imgur/imgur.component"; -import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-bot.component"; -import { ConfigScreenComplexBotComponent } from "./configs/complex-bot/config-screen/config-screen.complex-bot.component"; +import { + ConfigScreenComplexBotComponent +} from "./configs/complex-bot/config-screen/config-screen.complex-bot.component"; import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component"; import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component"; import { ConfigScreenBridgeComponent } from "./configs/bridge/config-screen/config-screen.bridge.component"; @@ -78,17 +79,23 @@ import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.co import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-picker/sticker-picker.component"; import { AdminTelegramApiService } from "./shared/services/admin/admin-telegram-api.service"; import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram.component"; -import { AdminTelegramBridgeManageSelfhostedComponent } from "./admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component"; +import { + AdminTelegramBridgeManageSelfhostedComponent +} from "./admin/bridges/telegram/manage-selfhosted/manage-selfhosted.component"; import { TelegramApiService } from "./shared/services/integrations/telegram-api.service"; import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component"; import { TelegramAskUnbridgeComponent } from "./configs/bridge/telegram/ask-unbridge/ask-unbridge.component"; import { TelegramCannotUnbridgeComponent } from "./configs/bridge/telegram/cannot-unbridge/cannot-unbridge.component"; -import { AdminWebhooksBridgeManageSelfhostedComponent } from "./admin/bridges/webhooks/manage-selfhosted/manage-selfhosted.component"; +import { + AdminWebhooksBridgeManageSelfhostedComponent +} from "./admin/bridges/webhooks/manage-selfhosted/manage-selfhosted.component"; import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component"; import { AdminWebhooksApiService } from "./shared/services/admin/admin-webhooks-api.service"; import { WebhooksApiService } from "./shared/services/integrations/webhooks-api.service"; import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component"; -import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; +import { + GenericFullscreenWidgetWrapperComponent +} from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; @@ -99,7 +106,9 @@ import { AdminCustomBotsComponent } from "./admin/custom-bots/custom-bots.compon import { AdminAddCustomBotComponent } from "./admin/custom-bots/add/add.component"; import { SlackApiService } from "./shared/services/integrations/slack-api.service"; import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component"; -import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component"; +import { + AdminSlackBridgeManageSelfhostedComponent +} from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component"; import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service"; import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component"; @@ -119,6 +128,30 @@ import { AdminWidgetWhiteboardConfigComponent } from "./admin/widgets/whiteboard import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component"; +import { + AdminHookshotGithubBridgeManageSelfhostedComponent +} from "./admin/bridges/hookshot-github/manage-selfhosted/manage-selfhosted.component"; +import { AdminHookshotGithubApiService } from "./shared/services/admin/admin-hookshot-github-api.service"; +import { HookshotGithubApiService } from "./shared/services/integrations/hookshot-github-api.service"; +import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component"; +import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component"; +import { + AdminHookshotJiraBridgeManageSelfhostedComponent +} from "./admin/bridges/hookshot-jira/manage-selfhosted/manage-selfhosted.component"; +import { AdminHookshotJiraApiService } from "./shared/services/admin/admin-hookshot-jira-api.service"; +import { HookshotJiraApiService } from "./shared/services/integrations/hookshot-jira-api.service"; +import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component"; +import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component"; +import { + AdminHookshotWebhookBridgeManageSelfhostedComponent +} from "./admin/bridges/hookshot-webhook/manage-selfhosted/manage-selfhosted.component"; +import { AdminHookshotWebhookApiService } from "./shared/services/admin/admin-hookshot-webhook-api.service"; +import { HookshotWebhookApiService } from "./shared/services/integrations/hookshot-webhook-api.service"; +import { + HookshotWebhookBridgeConfigComponent +} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component"; +import { FieldComponent } from "./elements/field/field.component"; // AoT requires an exported function for factories export function HttpLoaderFactory(http: HttpClient) { @@ -149,7 +182,7 @@ export function HttpLoaderFactory(http: HttpClient) { declarations: [ AppComponent, HomeComponent, - RiotComponent, + ElementComponent, IntegrationBagComponent, PageHeaderComponent, SpinnerComponent, @@ -162,7 +195,7 @@ export function HttpLoaderFactory(http: HttpClient) { BigBlueButtonWidgetWrapperComponent, GCalWidgetWrapperComponent, BigBlueButtonConfigComponent, - RiotHomeComponent, + ElementHomeComponent, IboxComponent, ConfigScreenWidgetComponent, CustomWidgetConfigComponent, @@ -185,7 +218,6 @@ export function HttpLoaderFactory(http: HttpClient) { AdminNebGuggyConfigComponent, AdminNebGoogleConfigComponent, AdminNebImgurConfigComponent, - ConfigSimpleBotComponent, ConfigScreenComplexBotComponent, RssComplexBotConfigComponent, TravisCiComplexBotConfigComponent, @@ -227,7 +259,17 @@ export function HttpLoaderFactory(http: HttpClient) { AdminTermsNewEditPublishDialogComponent, TermsWidgetWrapperComponent, WhiteboardWidgetComponent, - AdminWidgetWhiteboardConfigComponent + AdminWidgetWhiteboardConfigComponent, + AdminHookshotGithubBridgeComponent, + AdminHookshotGithubBridgeManageSelfhostedComponent, + HookshotGithubBridgeConfigComponent, + AdminHookshotJiraBridgeComponent, + AdminHookshotJiraBridgeManageSelfhostedComponent, + HookshotJiraBridgeConfigComponent, + AdminHookshotWebhookBridgeComponent, + AdminHookshotWebhookBridgeManageSelfhostedComponent, + HookshotWebhookBridgeConfigComponent, + FieldComponent, // Vendor ], @@ -257,6 +299,12 @@ export function HttpLoaderFactory(http: HttpClient) { AdminSlackApiService, ToasterService, AdminTermsApiService, + AdminHookshotGithubApiService, + HookshotGithubApiService, + AdminHookshotJiraApiService, + HookshotJiraApiService, + AdminHookshotWebhookApiService, + HookshotWebhookApiService, {provide: Window, useValue: window}, // Vendor @@ -270,7 +318,6 @@ export function HttpLoaderFactory(http: HttpClient) { AdminNebGuggyConfigComponent, AdminNebGoogleConfigComponent, AdminNebImgurConfigComponent, - ConfigSimpleBotComponent, AdminIrcBridgeNetworksComponent, AdminIrcBridgeAddSelfhostedComponent, AdminStickerPackPreviewComponent, @@ -282,7 +329,7 @@ export function HttpLoaderFactory(http: HttpClient) { AdminSlackBridgeManageSelfhostedComponent, AdminLogoutConfirmationDialogComponent, AdminTermsNewEditPublishDialogComponent, - AdminWidgetWhiteboardConfigComponent + AdminWidgetWhiteboardConfigComponent, ] }) export class AppModule { @@ -291,7 +338,7 @@ export class AppModule { } hmrOnInit(store) { - console.log("HMR store", store); + console.log("Dimension HMR store", store); } hmrOnDestroy(store) { diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 9e59efb..68dca2e 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -1,13 +1,13 @@ import { RouterModule, Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; -import { RiotComponent } from "./riot/riot.component"; +import { ElementComponent } from "./element/element.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 { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component"; import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component"; -import { RiotHomeComponent } from "./riot/riot-home/home.component"; +import { ElementHomeComponent } from "./element/element-home/home.component"; import { CustomWidgetConfigComponent } from "./configs/widget/custom/custom.widget.component"; import { EtherpadWidgetConfigComponent } from "./configs/widget/etherpad/etherpad.widget.component"; import { GoogleCalendarWidgetConfigComponent } from "./configs/widget/google-calendar/gcal.widget.component"; @@ -33,7 +33,9 @@ import { AdminTelegramBridgeComponent } from "./admin/bridges/telegram/telegram. import { TelegramBridgeConfigComponent } from "./configs/bridge/telegram/telegram.bridge.component"; import { AdminWebhooksBridgeComponent } from "./admin/bridges/webhooks/webhooks.component"; import { WebhooksBridgeConfigComponent } from "./configs/bridge/webhooks/webhooks.bridge.component"; -import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; +import { + GenericFullscreenWidgetWrapperComponent +} from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; @@ -48,19 +50,27 @@ import { AdminTermsComponent } from "./admin/terms/terms.component"; import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component"; import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component"; import { WhiteboardWidgetComponent } from "./configs/widget/whiteboard/whiteboard.widget.component"; +import { AdminHookshotGithubBridgeComponent } from "./admin/bridges/hookshot-github/hookshot-github.component"; +import { HookshotGithubBridgeConfigComponent } from "./configs/bridge/hookshot-github/hookshot-github.bridge.component"; +import { AdminHookshotJiraBridgeComponent } from "./admin/bridges/hookshot-jira/hookshot-jira.component"; +import { HookshotJiraBridgeConfigComponent } from "./configs/bridge/hookshot-jira/hookshot-jira.bridge.component"; +import { AdminHookshotWebhookBridgeComponent } from "./admin/bridges/hookshot-webhook/hookshot-webhook.component"; +import { + HookshotWebhookBridgeConfigComponent +} from "./configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component"; const routes: Routes = [ {path: "", component: HomeComponent}, - {path: "riot", pathMatch: "full", redirectTo: "riot-app"}, - {path: "element", pathMatch: "full", redirectTo: "riot-app"}, + {path: "riot", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Integration manager"}}, + {path: "element", pathMatch: "full", redirectTo: "riot-app", data: {breadcrumb: "Home", name: "Integration manager"}}, { path: "riot-app", - component: RiotComponent, - data: {breadcrumb: "Home", name: "Dimension"}, + component: ElementComponent, + data: {breadcrumb: "Home", name: "Integration manager"}, children: [ { path: "", - component: RiotHomeComponent, + component: ElementHomeComponent, }, { path: "admin", @@ -134,6 +144,21 @@ const routes: Routes = [ component: AdminSlackBridgeComponent, data: {breadcrumb: "Slack Bridge", name: "Slack Bridge"}, }, + { + path: "hookshot_github", + component: AdminHookshotGithubBridgeComponent, + data: {breadcrumb: "Github Bridge", name: "Github Bridge"}, + }, + { + path: "hookshot_jira", + component: AdminHookshotJiraBridgeComponent, + data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"}, + }, + { + path: "hookshot_webhook", + component: AdminHookshotWebhookBridgeComponent, + data: {breadcrumb: "Webhook Bridge", name: "Webhook Bridge"}, + }, ], }, { @@ -180,97 +205,114 @@ const routes: Routes = [ { path: "bigbluebutton", component: BigBlueButtonConfigComponent, - data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"}, + data: {breadcrumb: "BigBlueButton", name: "BigBlueButton"}, }, { path: "etherpad", component: EtherpadWidgetConfigComponent, - data: {breadcrumb: "Notes Widgets", name: "Notes Widgets"}, + data: {breadcrumb: "Etherpad", name: "Etherpad"}, }, { path: "googlecalendar", component: GoogleCalendarWidgetConfigComponent, - data: {breadcrumb: "Google Calendar Widgets", name: "Google Calendar Widgets"}, + data: {breadcrumb: "Google Calendar", name: "Google Calendar"}, }, { path: "googledocs", component: GoogleDocsWidgetConfigComponent, - data: {breadcrumb: "Google Doc Widgets", name: "Google Doc Widgets"}, + data: {breadcrumb: "Google Docs", name: "Google Docs"}, }, { path: "jitsi", component: JitsiWidgetConfigComponent, - data: {breadcrumb: "Jitsi Widgets", name: "Jitsi Widgets"}, + data: {breadcrumb: "Jitsi", name: "Jitsi"}, }, { path: "twitch", component: TwitchWidgetConfigComponent, - data: {breadcrumb: "Twitch Livestream Widgets", name: "Twitch Livestream Widgets"}, + data: {breadcrumb: "Twitch Livestream", name: "Twitch Livestream"}, }, { path: "youtube", component: YoutubeWidgetConfigComponent, - data: {breadcrumb: "Youtube Video Widgets", name: "Youtube Video Widgets"}, + data: {breadcrumb: "Youtube", name: "Youtube"}, }, { path: "grafana", component: GrafanaWidgetConfigComponent, - data: {breadcrumb: "Grafana Widgets", name: "Grafana Widgets"}, + data: {breadcrumb: "Grafana", name: "Grafana"}, }, { path: "tradingview", component: TradingViewWidgetConfigComponent, - data: {breadcrumb: "TradingView Widgets", name: "TradingView Widgets"}, + data: {breadcrumb: "TradingView", name: "TradingView"}, }, { path: "spotify", component: SpotifyWidgetConfigComponent, - data: {breadcrumb: "Spotify Widgets", name: "Spotify Widgets"}, + data: {breadcrumb: "Spotify", name: "Spotify"}, }, { path: "whiteboard", component: WhiteboardWidgetComponent, - data: {breadcrumb: "Whiteboard Widgets", name: "Whiteboard Widgets"}, + data: {breadcrumb: "Whiteboard", name: "Whiteboard"}, }, ], }, { path: "complex-bot", + data: {breadcrumb: {skip: true}}, children: [ { path: "rss", component: RssComplexBotConfigComponent, - data: {breadcrumb: "RSS Bot Configuration", name: "RSS Bot Configuration"}, + data: {breadcrumb: "RSS Bot", name: "RSS Bot"}, }, { path: "travisci", component: TravisCiComplexBotConfigComponent, - data: {breadcrumb: "Travis CI Configuration", name: "Travis CI Configuration"}, + data: {breadcrumb: "Travis CI", name: "Travis CI"}, }, ], }, { path: "bridge", + data: {breadcrumb: {skip: true}}, children: [ { path: "irc", component: IrcBridgeConfigComponent, - data: {breadcrumb: "IRC Bridge Configuration", name: "IRC Bridge Configuration"}, + data: {breadcrumb: "IRC Bridge", name: "IRC Bridge"}, }, { path: "telegram", component: TelegramBridgeConfigComponent, - data: {breadcrumb: "Telegram Bridge Configuration", name: "Telegram Bridge Configuration"}, + data: {breadcrumb: "Telegram Bridge", name: "Telegram Bridge"}, }, { path: "webhooks", component: WebhooksBridgeConfigComponent, - data: {breadcrumb: "Webhook Bridge Configuration", name: "Webhook Bridge Configuration"}, + data: {breadcrumb: "Webhooks Bridge", name: "Webhooks Bridge"}, }, { path: "slack", component: SlackBridgeConfigComponent, - data: {breadcrumb: "Slack Bridge Configuration", name: "Slack Bridge Configuration"}, + data: {breadcrumb: "Slack Bridge", name: "Slack Bridge"}, + }, + { + path: "hookshot_github", + component: HookshotGithubBridgeConfigComponent, + data: {breadcrumb: "Github Bridge", name: "Github Bridge"}, + }, + { + path: "hookshot_jira", + component: HookshotJiraBridgeConfigComponent, + data: {breadcrumb: "Jira Bridge", name: "Jira Bridge"}, + }, + { + path: "hookshot_webhook", + component: HookshotWebhookBridgeConfigComponent, + data: {breadcrumb: "Webhooks Bridge", name: "Webhooks Bridge"}, }, ], }, @@ -283,6 +325,7 @@ const routes: Routes = [ }, { path: "widgets", + data: {breadcrumb: {skip: true}}, children: [ {path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent}, {path: "generic", component: GenericWidgetWrapperComponent}, diff --git a/web/app/configs/bridge/bridge.component.ts b/web/app/configs/bridge/bridge.component.ts index d81fa98..02d97c1 100644 --- a/web/app/configs/bridge/bridge.component.ts +++ b/web/app/configs/bridge/bridge.component.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Input, OnDestroy, OnInit } from "@angular/core"; +import { Inject, Injectable, OnDestroy, OnInit } from "@angular/core"; import { FE_Bridge } from "../../shared/models/integration"; import { ActivatedRoute } from "@angular/router"; import { Subscription } from "rxjs/Subscription"; diff --git a/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html new file mode 100644 index 0000000..70074f6 --- /dev/null +++ b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html @@ -0,0 +1,59 @@ + + + +
+
+

{{'This room is bridged to' | translate}} {{bridgedRepoSlug}}

+ +
+
+

+ {{'In order to bridge to Github, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}} +

+ + {{'Sign in with GitHub' | translate}} + +
+
+

+ {{'Almost there! Just need to add the bot to your organizations now.' | translate}} +

+ + {{'Add to GitHub' | translate}} + +
+ +
+
+
diff --git a/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.scss b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.scss new file mode 100644 index 0000000..9e914bf --- /dev/null +++ b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.scss @@ -0,0 +1,4 @@ +.actions-col { + width: 120px; + text-align: center; +} diff --git a/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.ts b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.ts new file mode 100644 index 0000000..a2cd5ae --- /dev/null +++ b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.ts @@ -0,0 +1,176 @@ +import { Component, OnInit } from "@angular/core"; +import { BridgeComponent } from "../bridge.component"; +import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { TranslateService } from "@ngx-translate/core"; +import { FE_HookshotGithubConnection, FE_HookshotGithubOrgReposDto } from "../../../shared/models/hookshot_github"; +import { HookshotGithubApiService } from "../../../shared/services/integrations/hookshot-github-api.service"; + +interface HookshotConfig { + botUserId: string; + connections: FE_HookshotGithubConnection[]; +} + +@Component({ + templateUrl: "hookshot-github.bridge.component.html", + styleUrls: ["hookshot-github.bridge.component.scss"], +}) +export class HookshotGithubBridgeConfigComponent extends BridgeComponent implements OnInit { + + public isBusy: boolean; + public authUrl: SafeUrl; + public orgAuthUrl: SafeUrl; + public loadingConnections = true; + public bridgedRepoSlug: string; + public orgEditAuthUrl: SafeUrl; + public orgAddAuthUrl: SafeUrl; + + public orgs: string[] = []; + public orgId: string; + + public repos: string[] = []; + public repoId: string; + + private timerId: any; + private orgToRepoMap: Record; + + constructor(private hookshot: HookshotGithubApiService, private scalar: ScalarClientApiService, private sanitizer: DomSanitizer, public translate: TranslateService) { + super("hookshot_github", translate); + this.translate = translate; + } + + public ngOnInit() { + super.ngOnInit(); + + this.loadingConnections = true; + this.tryLoadOrgs(); + + this.hookshot.getAuthUrls().then(urls => { + this.orgAddAuthUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.orgUrl); + }); + } + + private tryOrgAuth() { + this.hookshot.getAuthUrls().then(urls => { + this.orgAuthUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.orgUrl); + this.loadingConnections = false; + this.timerId = setTimeout(() => { + this.tryLoadOrgs(); + }, 1000); + }); + } + + private tryLoadOrgs() { + this.hookshot.getInstalledLocations().then(r => { + this.authUrl = null; + + if (r.length <= 0) { + this.tryOrgAuth(); + return; + } + + this.orgToRepoMap = {}; + for (const orgDto of r) { + this.orgToRepoMap[orgDto.organization.name] = orgDto; + } + + this.orgs = Object.keys(this.orgToRepoMap); + this.orgId = this.orgs[0]; + this.orgAuthUrl = null; + this.loadRepos(); + + if (this.timerId) { + clearTimeout(this.timerId); + } + }).catch(e => { + if (e.status === 403 && e.error.dim_errcode === "T2B_NOT_LOGGED_IN") { + this.hookshot.getAuthUrls().then(urls => { + this.authUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urls.userUrl); + this.loadingConnections = false; + this.timerId = setTimeout(() => { + this.tryLoadOrgs(); + }, 1000); + }); + } else if (e.status === 400 && e.error.dim_errcode === "T2B_MISSING_AUTH") { + this.tryOrgAuth(); + } else { + console.error(e); + this.translate.get('Error getting Github information').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }); + } + + public loadRepos() { + this.isBusy = true; + + const dto = this.orgToRepoMap[this.orgId]; + this.repos = dto.repositories.map(r => r.name); + this.repoId = this.repos[0]; + this.orgEditAuthUrl = dto.changeSelectionUrl ? this.sanitizer.bypassSecurityTrustResourceUrl(dto.changeSelectionUrl) : null; + + if (this.isBridged) { + const conn = this.bridge.config.connections[0].config; + this.bridgedRepoSlug = `${conn.org}/${conn.repo}`; + } + + this.isBusy = false; + this.loadingConnections = false; + } + + public get isBridged(): boolean { + return this.bridge.config.connections.length > 0; + } + + public async bridgeRoom(): Promise { + this.isBusy = true; + + try { + await this.scalar.inviteUser(this.roomId, this.bridge.config.botUserId); + } catch (e) { + if (!e.response || !e.response.error || !e.response.error._error || + e.response.error._error.message.indexOf("already in the room") === -1) { + this.isBusy = false; + this.translate.get('Error inviting bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + return; + } + } + + await this.scalar.setUserPowerLevel(this.roomId, this.bridge.config.botUserId, 50); + + this.hookshot.bridgeRoom(this.roomId, this.orgId, this.repoId).then(conn => { + this.bridge.config.connections.push(conn); + this.loadRepos(); + this.isBusy = false; + this.translate.get('Bridge requested').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.translate.get('Error requesting bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + + public unbridgeRoom(): void { + this.isBusy = true; + this.hookshot.unbridgeRoom(this.roomId).then(() => { + this.bridge.config.connections = []; + this.isBusy = false; + this.translate.get('Bridge removed').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.translate.get('Error removing bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } +} diff --git a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html new file mode 100644 index 0000000..6fe6165 --- /dev/null +++ b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.html @@ -0,0 +1,41 @@ + + + +
+
+

{{'This room is bridged to' | translate}} {{bridgedProjectUrlUnsafe}}

+ +
+
+

+ {{'In order to bridge to Jira, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}} +

+ + {{'Sign in with Jira' | translate}} + +
+
+ + + +
+
+
+
diff --git a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.scss b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.scss new file mode 100644 index 0000000..9e914bf --- /dev/null +++ b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.scss @@ -0,0 +1,4 @@ +.actions-col { + width: 120px; + text-align: center; +} diff --git a/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts new file mode 100644 index 0000000..42a8012 --- /dev/null +++ b/web/app/configs/bridge/hookshot-jira/hookshot-jira.bridge.component.ts @@ -0,0 +1,163 @@ +import { Component, OnInit } from "@angular/core"; +import { BridgeComponent } from "../bridge.component"; +import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { TranslateService } from "@ngx-translate/core"; +import { HookshotJiraApiService } from "../../../shared/services/integrations/hookshot-jira-api.service"; +import { + FE_HookshotJiraConnection, + FE_HookshotJiraInstance, + FE_HookshotJiraProject +} from "../../../shared/models/hookshot_jira"; + +interface HookshotConfig { + botUserId: string; + connections: FE_HookshotJiraConnection[]; + loggedIn: boolean; + instances?: FE_HookshotJiraInstance[]; +} + +@Component({ + templateUrl: "hookshot-jira.bridge.component.html", + styleUrls: ["hookshot-jira.bridge.component.scss"], +}) +export class HookshotJiraBridgeConfigComponent extends BridgeComponent implements OnInit { + + public isBusy: boolean; + public authUrl: SafeUrl; + public loadingConnections = true; + public bridgedProjectUrl: SafeUrl; + public bridgedProjectUrlUnsafe: string; + + public instances: FE_HookshotJiraInstance[] = []; + public instance: string; + + public projects: FE_HookshotJiraProject[] = []; + public project: string; + + private timerId: any; + + constructor(private hookshot: HookshotJiraApiService, private scalar: ScalarClientApiService, private sanitizer: DomSanitizer, public translate: TranslateService) { + super("hookshot_jira", translate); + } + + public get instanceOptions(): {key: string, value: string}[] { + return this.instances.map(i => ({key: i.name, value: `${i.name} (${i.url})`})); + } + + public get projectOptions(): {key: string, value: string}[] { + return this.projects.map(p => ({key: p.key, value: `${p.key} (${p.name})`})); + } + + public ngOnInit() { + super.ngOnInit(); + + this.loadingConnections = true; + this.tryLoadInstances(); + } + + private tryLoadInstances() { + this.hookshot.getInstances().then(r => { + this.authUrl = null; + this.instances = r; + this.instance = this.instances[0].name; + this.loadProjects(); + + if (this.timerId) { + clearTimeout(this.timerId); + } + }).catch(e => { + if (e.status === 403 && e.error.dim_errcode === "T2B_NOT_LOGGED_IN") { + this.hookshot.getAuthUrl().then(url => { + this.authUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); + this.loadingConnections = false; + this.timerId = setTimeout(() => { + this.tryLoadInstances(); + }, 1000); + }); + } else { + console.error(e); + this.translate.get('Error getting Jira information').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + } + }); + } + + public loadProjects() { + this.isBusy = true; + this.hookshot.getProjects(this.instance).then(projects => { + this.projects = projects; + this.project = this.projects[0].key; + + if (this.isBridged) { + this.bridgedProjectUrlUnsafe = this.bridge.config.connections[0].config.url; + this.bridgedProjectUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.bridgedProjectUrlUnsafe); + } + + this.isBusy = false; + this.loadingConnections = false; + }).catch(e => { + console.error(e); + this.isBusy = false; + this.translate.get('Error getting Jira information').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + + public get isBridged(): boolean { + return this.bridge.config.connections.length > 0; + } + + public async bridgeRoom(): Promise { + this.isBusy = true; + + try { + await this.scalar.inviteUser(this.roomId, this.bridge.config.botUserId); + } catch (e) { + if (!e.response || !e.response.error || !e.response.error._error || + e.response.error._error.message.indexOf("already in the room") === -1) { + this.isBusy = false; + this.translate.get('Error inviting bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + return; + } + } + + await this.scalar.setUserPowerLevel(this.roomId, this.bridge.config.botUserId, 50); + + this.hookshot.bridgeRoom(this.roomId, this.instance, this.project).then(conn => { + this.bridge.config.connections.push(conn); + this.loadProjects(); + this.isBusy = false; + this.translate.get('Bridge requested').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.translate.get('Error requesting bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + + public unbridgeRoom(): void { + this.isBusy = true; + this.hookshot.unbridgeRoom(this.roomId).then(() => { + this.bridge.config.connections = []; + this.isBusy = false; + this.translate.get('Bridge removed').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(error => { + this.isBusy = false; + console.error(error); + this.translate.get('Error removing bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } +} diff --git a/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.html b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.html new file mode 100644 index 0000000..86dab2a --- /dev/null +++ b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.html @@ -0,0 +1,29 @@ + + +

{{'New webhook' | translate}}

+
+ + +
+

{{'Webhooks' | translate}}

+

{{'No webhooks configured for this room.' | translate}}

+ +
+ {{hook.config.name || ('No name' | translate)}} + URL: {{ hook.config.url }} +
+ +
+
+
+
diff --git a/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.scss b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.scss new file mode 100644 index 0000000..cedec6b --- /dev/null +++ b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.scss @@ -0,0 +1,3 @@ +.webhook-url { + word-break: break-word; +} \ No newline at end of file diff --git a/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.ts b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.ts new file mode 100644 index 0000000..0578fb0 --- /dev/null +++ b/web/app/configs/bridge/hookshot-webhook/hookshot-webhook.bridge.component.ts @@ -0,0 +1,75 @@ +import { Component } from "@angular/core"; +import { BridgeComponent } from "../bridge.component"; +import { ScalarClientApiService } from "../../../shared/services/scalar/scalar-client-api.service"; +import { TranslateService } from "@ngx-translate/core"; +import { FE_HookshotWebhookConnection } from "../../../shared/models/hookshot_webhook"; +import { HookshotWebhookApiService } from "../../../shared/services/integrations/hookshot-webhook-api.service"; + +interface HookshotConfig { + botUserId: string; + connections: FE_HookshotWebhookConnection[]; +} + +@Component({ + templateUrl: "hookshot-webhook.bridge.component.html", + styleUrls: ["hookshot-webhook.bridge.component.scss"], +}) +export class HookshotWebhookBridgeConfigComponent extends BridgeComponent { + + public webhookName: string; + public isBusy = false; + + constructor(private webhooks: HookshotWebhookApiService, private scalar: ScalarClientApiService, public translate: TranslateService) { + super("hookshot_webhook", translate); + } + + public async newHook() { + this.isBusy = true; + + try { + await this.scalar.inviteUser(this.roomId, this.newConfig.botUserId); + } catch (e) { + if (!e.response || !e.response.error || !e.response.error._error || + e.response.error._error.message.indexOf("already in the room") === -1) { + this.isBusy = false; + this.translate.get('Error inviting bridge').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + return; + } + } + + this.webhooks.createWebhook(this.roomId, this.webhookName).then(hook => { + this.newConfig.connections.push(hook); + this.isBusy = false; + this.webhookName = ""; + this.translate.get('Webhook created').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(err => { + console.error(err); + this.isBusy = false; + this.translate.get('Error creating webhook').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } + + public removeHook(hook: FE_HookshotWebhookConnection) { + this.isBusy = true; + this.webhooks.deleteWebhook(this.roomId, hook.id).then(() => { + const idx = this.newConfig.connections.indexOf(hook); + if (idx !== -1) this.newConfig.connections.splice(idx, 1); + this.isBusy = false; + this.translate.get('Webhook deleted').subscribe((res: string) => { + this.toaster.pop("success", res); + }); + }).catch(err => { + console.error(err); + this.isBusy = false; + this.translate.get('Error deleting webhook').subscribe((res: string) => { + this.toaster.pop("error", res); + }); + }); + } +} diff --git a/web/app/configs/bridge/irc/irc.bridge.component.html b/web/app/configs/bridge/irc/irc.bridge.component.html index 2d400c4..c76162f 100644 --- a/web/app/configs/bridge/irc/irc.bridge.component.html +++ b/web/app/configs/bridge/irc/irc.bridge.component.html @@ -1,90 +1,54 @@ - -
- {{'Add an IRC channel' | translate}} -
-
-
- {{'Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.' | translate}} -
+

{{'Add IRC channel' | translate}}

+
+ {{'Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.' | translate}} +
+
+ + + +
-
- - -
-
#
- -
-
- -
-
+
+

{{'The person selected here will be asked to approve or deny the bridge request.' | translate}}

+ + +
-
- -
- -
-
+

{{'Bridged channels' | translate}}

+

{{'No channels are bridged to this room.' | translate}}

+
+ {{channel.name + ' on ' + channel.networkName + (channel.pending ? ' (pending)' : '')}} +
+
- - -
- {{'IRC Networks' | translate}} -
-
- - - - - - - - - - - - - - - - - - -
{{'Channel' | translate}}{{'Network' | translate}}{{'Actions' | translate}}
{{'No bridged channels' | translate}}
- {{ channel.name }} - (pending) - {{ channel.networkName }} - -
-
-
+
diff --git a/web/app/configs/bridge/irc/irc.bridge.component.ts b/web/app/configs/bridge/irc/irc.bridge.component.ts index 913dc0f..abe25f3 100644 --- a/web/app/configs/bridge/irc/irc.bridge.component.ts +++ b/web/app/configs/bridge/irc/irc.bridge.component.ts @@ -41,17 +41,17 @@ export class IrcBridgeConfigComponent extends BridgeComponent { } private resetForm() { - this.networkId = this.getNetworks()[0].id; + this.networkId = this.getNetworks()[0].key; this.channel = ""; this.ops = []; this.channelStep = 1; } - public getNetworks(): { id: string, name: string }[] { + public getNetworks(): { key: string, value: string }[] { const ids = Object.keys(this.bridge.config.availableNetworks); if (!this.networkId) setTimeout(() => this.networkId = ids[0], 0); return ids.map(i => { - return {id: i, name: this.bridge.config.availableNetworks[i].name}; + return {key: i, value: this.bridge.config.availableNetworks[i].name}; }); } diff --git a/web/app/configs/bridge/slack/slack.bridge.component.html b/web/app/configs/bridge/slack/slack.bridge.component.html index 41ebdf0..3382d85 100644 --- a/web/app/configs/bridge/slack/slack.bridge.component.html +++ b/web/app/configs/bridge/slack/slack.bridge.component.html @@ -1,57 +1,48 @@ - -
- {{'Bridge to Slack' | translate}} -
-
- + +
+
+

{{'This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn\'t support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.' | translate}}

+
-
-
- {{'This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn\'t support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.' | translate}} -
- -
-
- This room is bridged to "{{ bridge.config.link.channelName }}" on Slack. - -
-
-

- {{'In order to bridge Slack channels, you\'ll need to authorize the bridge to access your teams and channels. Please click the button below to do so.' | translate}} -

- - sign in with slack - -
-
- - - -
+
+

{{'This room is bridged to ' | translate}}"{{ bridge.config.link.channelName }}"{{' on Slack.' | translate}}

+
- +
+

+ {{'In order to bridge Slack channels, you\'ll need to authorize the bridge to access your teams and channels. Please click the button below to do so.' | translate}} +

+ + sign in with slack + +
+
+ + + +
+
+ diff --git a/web/app/configs/bridge/slack/slack.bridge.component.ts b/web/app/configs/bridge/slack/slack.bridge.component.ts index 746d6ea..d18a53a 100644 --- a/web/app/configs/bridge/slack/slack.bridge.component.ts +++ b/web/app/configs/bridge/slack/slack.bridge.component.ts @@ -33,6 +33,14 @@ export class SlackBridgeConfigComponent extends BridgeComponent imp this.translate = translate; } + public get teamOptions(): {key: string, value: string}[] { + return this.teams.map(t => ({key: t.id, value: t.name})); + } + + public get channelOptions(): {key: string, value: string}[] { + return this.channels.map(t => ({key: t.id, value: t.name})); + } + public ngOnInit() { super.ngOnInit(); diff --git a/web/app/configs/bridge/telegram/telegram.bridge.component.html b/web/app/configs/bridge/telegram/telegram.bridge.component.html index 51c94f4..5e3272e 100644 --- a/web/app/configs/bridge/telegram/telegram.bridge.component.html +++ b/web/app/configs/bridge/telegram/telegram.bridge.component.html @@ -1,34 +1,29 @@ - -
- {{'Bridge to Telegram' | translate}} -
-
-
- {{'This room is bridged to on Telegram' | translate}} "{{ chatName }}" ({{ chatId }}) {{'on Telegram' | translate}}. -
- -
- - {{'You do not have the necessary permissions in this room to unbridge the channel.' | translate}} - -
-
- - -
+
+ {{'This room is bridged to on Telegram' | translate}} "{{ chatName }}" ({{ chatId }}) {{'on Telegram' | translate}}. +
+
- + + {{'You do not have the necessary permissions in this room to unbridge the channel.' | translate}} + +
+
+

{{'After inviting' | translate}} @{{ botUsername }} {{'to your Telegram chat, run the command' | translate}} /id {{'in the Telegram room to get the chat ID.' | translate}}

+ + + +
diff --git a/web/app/configs/bridge/telegram/telegram.bridge.component.ts b/web/app/configs/bridge/telegram/telegram.bridge.component.ts index 3d626a4..8b6e223 100644 --- a/web/app/configs/bridge/telegram/telegram.bridge.component.ts +++ b/web/app/configs/bridge/telegram/telegram.bridge.component.ts @@ -3,9 +3,7 @@ import { BridgeComponent } from "../bridge.component"; import { TelegramApiService } from "../../../shared/services/integrations/telegram-api.service"; import { FE_PortalInfo } from "../../../shared/models/telegram"; import { TelegramAskUnbridgeComponent } from "./ask-unbridge/ask-unbridge.component"; -import { - TelegramCannotUnbridgeComponent -} from "./cannot-unbridge/cannot-unbridge.component"; +import { TelegramCannotUnbridgeComponent } from "./cannot-unbridge/cannot-unbridge.component"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; @@ -60,19 +58,19 @@ export class TelegramBridgeConfigComponent extends BridgeComponent - -
- {{'Add a new webhook' | translate}} -
-
- -
- -
+

{{'New webhook' | translate}}

+
+ + +
+

{{'Webhooks' | translate}}

+

{{'No webhooks configured for this room.' | translate}}

+ +
+ {{hook.label || ('No name' | translate)}} + URL: {{ hook.url }} +
+
- - -
- Webhooks -
-
- - - - - - - - - - - - - - - - - - - - - -
Name{{'Type' | translate}}URL{{'Actions' | translate}}
{{'No webhooks' | translate}}
{{ hook.label }}{{'No name' | translate}}{{ hook.type }}{{ hook.url }} - -
-
-
+
diff --git a/web/app/configs/simple-bot/simple-bot.component.html b/web/app/configs/simple-bot/simple-bot.component.html deleted file mode 100644 index c83dde2..0000000 --- a/web/app/configs/simple-bot/simple-bot.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html index 5c2a3d8..9f66cb6 100644 --- a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html @@ -1,11 +1,14 @@ - + - + diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts index fcfda26..3d71d97 100644 --- a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts @@ -1,5 +1,5 @@ -import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component"; -import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget"; +import { DISABLE_AUTOMATIC_WRAPPING, WidgetComponent } from "../widget.component"; +import { EditableWidget, WIDGET_BIGBLUEBUTTON } from "../../../shared/models/widget"; import { Component } from "@angular/core"; import { FE_BigBlueButtonWidget } from "../../../shared/models/integration"; import { SessionStorage } from "../../../shared/SessionStorage"; diff --git a/web/app/configs/widget/config-screen/config-screen.widget.component.html b/web/app/configs/widget/config-screen/config-screen.widget.component.html index 385640b..dec9676 100644 --- a/web/app/configs/widget/config-screen/config-screen.widget.component.html +++ b/web/app/configs/widget/config-screen/config-screen.widget.component.html @@ -2,44 +2,33 @@
- -
- {{ widgetComponent.defaultName | translate }} {{'Add' | translate}} -
-
-
- - -
- -
-
+

{{addTitle}}

+
+ +
+
- +
- -
- {{ widget.name || widget.url || widgetComponent.defaultName }} - - {{ widget.data.title }} -
-
+
+

{{editTitle}}

+
+ {{widget.name || widget.url || widgetComponent.defaultName}}
-
- -
-
- + +
diff --git a/web/app/configs/widget/config-screen/config-screen.widget.component.ts b/web/app/configs/widget/config-screen/config-screen.widget.component.ts index 1f62119..37bc7f3 100644 --- a/web/app/configs/widget/config-screen/config-screen.widget.component.ts +++ b/web/app/configs/widget/config-screen/config-screen.widget.component.ts @@ -8,8 +8,9 @@ import { Component, ContentChild, Input, TemplateRef } from "@angular/core"; }) export class ConfigScreenWidgetComponent { @Input() widgetComponent: WidgetComponent; - @ContentChild(TemplateRef, { static: false }) - widgetParamsTemplate: TemplateRef; + @Input() addTitle: string; + @Input() editTitle: string; + @ContentChild(TemplateRef, { static: false }) widgetParamsTemplate: TemplateRef; constructor() {} } diff --git a/web/app/configs/widget/custom/custom.widget.component.html b/web/app/configs/widget/custom/custom.widget.component.html index cd24296..9c2ab75 100644 --- a/web/app/configs/widget/custom/custom.widget.component.html +++ b/web/app/configs/widget/custom/custom.widget.component.html @@ -1,18 +1,20 @@ - + - - + + diff --git a/web/app/configs/widget/etherpad/etherpad.widget.component.html b/web/app/configs/widget/etherpad/etherpad.widget.component.html index 38437ab..c403393 100644 --- a/web/app/configs/widget/etherpad/etherpad.widget.component.html +++ b/web/app/configs/widget/etherpad/etherpad.widget.component.html @@ -1,19 +1,21 @@ - + - - + + diff --git a/web/app/configs/widget/etherpad/etherpad.widget.component.ts b/web/app/configs/widget/etherpad/etherpad.widget.component.ts index 991ad69..05c272c 100644 --- a/web/app/configs/widget/etherpad/etherpad.widget.component.ts +++ b/web/app/configs/widget/etherpad/etherpad.widget.component.ts @@ -20,7 +20,6 @@ export class EtherpadWidgetConfigComponent extends WidgetComponent { } protected OnWidgetsDiscovered(widgets: EditableWidget[]): void { - console.log(widgets); for (const widget of widgets) { if (!widget.dimension.newUrl.startsWith("http://") && !widget.dimension.newUrl.startsWith("https://")) { const parsedUrl = url.parse(widget.url, true); diff --git a/web/app/configs/widget/google-calendar/gcal.widget.component.html b/web/app/configs/widget/google-calendar/gcal.widget.component.html index 6213630..d1719b9 100644 --- a/web/app/configs/widget/google-calendar/gcal.widget.component.html +++ b/web/app/configs/widget/google-calendar/gcal.widget.component.html @@ -1,11 +1,14 @@ - + - + diff --git a/web/app/configs/widget/google-docs/gdoc.widget.component.html b/web/app/configs/widget/google-docs/gdoc.widget.component.html index 4ca6cf9..ac61430 100644 --- a/web/app/configs/widget/google-docs/gdoc.widget.component.html +++ b/web/app/configs/widget/google-docs/gdoc.widget.component.html @@ -1,11 +1,14 @@ - + - + diff --git a/web/app/configs/widget/grafana/grafana.widget.component.html b/web/app/configs/widget/grafana/grafana.widget.component.html index 25bc58f..d9a4896 100644 --- a/web/app/configs/widget/grafana/grafana.widget.component.html +++ b/web/app/configs/widget/grafana/grafana.widget.component.html @@ -1,19 +1,21 @@ - + - - +

{{'To get a URL, go to Grafana and click \'share\' on a graph.' | translate}}

+ +
diff --git a/web/app/configs/widget/jitsi/jitsi.widget.component.html b/web/app/configs/widget/jitsi/jitsi.widget.component.html index e69853a..e04bf6b 100644 --- a/web/app/configs/widget/jitsi/jitsi.widget.component.html +++ b/web/app/configs/widget/jitsi/jitsi.widget.component.html @@ -1,18 +1,14 @@ - + - - + diff --git a/web/app/configs/widget/spotify/spotify.widget.component.html b/web/app/configs/widget/spotify/spotify.widget.component.html index 3008f45..a6fd40b 100644 --- a/web/app/configs/widget/spotify/spotify.widget.component.html +++ b/web/app/configs/widget/spotify/spotify.widget.component.html @@ -1,12 +1,15 @@ - + - +

{{'Click \'share\' from your favourite playlist, artist, track, or album and paste the Spotify URI here.' | translate}}

+
diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.html b/web/app/configs/widget/tradingview/tradingview.widget.component.html index 9cadad7..2dd4b6e 100644 --- a/web/app/configs/widget/tradingview/tradingview.widget.component.html +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.html @@ -1,23 +1,22 @@ - + - - - + + diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.ts b/web/app/configs/widget/tradingview/tradingview.widget.component.ts index 0f526ad..5d61222 100644 --- a/web/app/configs/widget/tradingview/tradingview.widget.component.ts +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.ts @@ -10,68 +10,68 @@ import { TranslateService } from "@ngx-translate/core"; export class TradingViewWidgetConfigComponent extends WidgetComponent { public readonly intervals = [ - {value: '1', label: '1 Minute'}, - {value: '3', label: '3 Minutes'}, - {value: '5', label: '5 Minutes'}, - {value: '15', label: '15 Minutes'}, - {value: '30', label: '30 Minutes'}, - {value: '60', label: '1 Hour'}, - {value: '120', label: '2 Hours'}, - {value: '180', label: '3 Hours'}, - {value: '240', label: '4 Hours'}, - {value: 'D', label: '1 Day'}, - {value: 'W', label: '1 Week'}, + {key: '1', value: '1 Minute'}, + {key: '3', value: '3 Minutes'}, + {key: '5', value: '5 Minutes'}, + {key: '15', value: '15 Minutes'}, + {key: '30', value: '30 Minutes'}, + {key: '60', value: '1 Hour'}, + {key: '120', value: '2 Hours'}, + {key: '180', value: '3 Hours'}, + {key: '240', value: '4 Hours'}, + {key: 'D', value: '1 Day'}, + {key: 'W', value: '1 Week'}, ]; public readonly pairs = [ // USD - {value: 'COINBASE:BTCUSD', label: 'Bitcoin / US Dollar'}, - {value: 'COINBASE:ETHUSD', label: 'Ethereum / US Dollar'}, - {value: 'COINBASE:LTCUSD', label: 'Litecoin / US Dollar'}, - {value: 'BITTREX:SNTUSD', label: 'Status Network Token / US Dollar'}, - {value: 'BITTREX:ETCUSD', label: 'Ethereum Classic / US Dollar'}, - {value: 'BITFINEX:BTGUSD', label: 'BTG / US Dollar'}, - {value: 'BITTREX:DASHUSD', label: 'Dash / US Dollar'}, - {value: 'BITFINEX:EOSUSD', label: 'EOS / US Dollar'}, - {value: 'BITFINEX:IOTUSD', label: 'IOTA / US Dollar'}, - {value: 'BITTREX:LSKUSD', label: 'Lisk / US Dollar'}, - {value: 'BITTREX:OMGUSD', label: 'OmiseGo / US Dollar'}, - {value: 'BITTREX:NEOUSD', label: 'NEO / US Dollar'}, - {value: 'BITTREX:XRPUSD', label: 'Ripple / US Dollar'}, - {value: 'BITFINEX:ZECUSD', label: 'Zcash / US Dollar'}, - {value: 'BITFINEX:XMRUSD', label: 'Monero / US Dollar'}, + {key: 'COINBASE:BTCUSD', value: 'Bitcoin / US Dollar'}, + {key: 'COINBASE:ETHUSD', value: 'Ethereum / US Dollar'}, + {key: 'COINBASE:LTCUSD', value: 'Litecoin / US Dollar'}, + {key: 'BITTREX:SNTUSD', value: 'Status Network Token / US Dollar'}, + {key: 'BITTREX:ETCUSD', value: 'Ethereum Classic / US Dollar'}, + {key: 'BITFINEX:BTGUSD', value: 'BTG / US Dollar'}, + {key: 'BITTREX:DASHUSD', value: 'Dash / US Dollar'}, + {key: 'BITFINEX:EOSUSD', value: 'EOS / US Dollar'}, + {key: 'BITFINEX:IOTUSD', value: 'IOTA / US Dollar'}, + {key: 'BITTREX:LSKUSD', value: 'Lisk / US Dollar'}, + {key: 'BITTREX:OMGUSD', value: 'OmiseGo / US Dollar'}, + {key: 'BITTREX:NEOUSD', value: 'NEO / US Dollar'}, + {key: 'BITTREX:XRPUSD', value: 'Ripple / US Dollar'}, + {key: 'BITFINEX:ZECUSD', value: 'Zcash / US Dollar'}, + {key: 'BITFINEX:XMRUSD', value: 'Monero / US Dollar'}, // Euro / GBP - {value: 'COINBASE:BTCEUR', label: 'Bitcoin / Euro'}, - {value: 'COINBASE:ETHEUR', label: 'Ethereum / Euro'}, - {value: 'COINBASE:LTCEUR', label: 'Litecoin / Euro'}, - {value: 'COINBASE:BTCGBP', label: 'Bitcoin / GBP'}, + {key: 'COINBASE:BTCEUR', value: 'Bitcoin / Euro'}, + {key: 'COINBASE:ETHEUR', value: 'Ethereum / Euro'}, + {key: 'COINBASE:LTCEUR', value: 'Litecoin / Euro'}, + {key: 'COINBASE:BTCGBP', value: 'Bitcoin / GBP'}, // Bitcoin - {value: 'COINBASE:ETHBTC', label: 'Ethereum / Bitcoin'}, - {value: 'COINBASE:LTCBTC', label: 'Litecoin / Bitcoin'}, - {value: 'BITTREX:SNTBTC', label: 'Status Network Token / Bitcoin'}, - {value: 'BITTREX:BCCBTC', label: 'Bitcoin Cash / Bitcoin'}, - {value: 'BITTREX:ADABTC', label: 'Ada / Bitcoin'}, - {value: 'BITTREX:ARKBTC', label: 'Ark / Bitcoin'}, - {value: 'BITTREX:EMC2BTC', label: 'Einsteinium / Bitcoin'}, - {value: 'BITFINEX:IOTBTC', label: 'IOTA / Bitcoin'}, - {value: 'BITTREX:LSKBTC', label: 'Lisk / Bitcoin'}, - {value: 'BITTREX:NEOBTC', label: 'Neo / Bitcoin'}, - {value: 'BITTREX:OMGBTC', label: 'OmiseGO / Bitcoin'}, - {value: 'BITTREX:POWRBTC', label: 'PowerLedger / Bitcoin'}, - {value: 'BITTREX:STRATBTC', label: 'Stratis / Bitcoin'}, - {value: 'BITTREX:TRIGBTC', label: 'TRIG Token / Bitcoin'}, - {value: 'BITTREX:VTCBTC', label: 'Vertcoin / Bitcoin'}, - {value: 'BITTREX:XLMBTC', label: 'Lumen / Bitcoin'}, - {value: 'BITTREX:XRPBTC', label: 'Ripple / Bitcoin'}, + {key: 'COINBASE:ETHBTC', value: 'Ethereum / Bitcoin'}, + {key: 'COINBASE:LTCBTC', value: 'Litecoin / Bitcoin'}, + {key: 'BITTREX:SNTBTC', value: 'Status Network Token / Bitcoin'}, + {key: 'BITTREX:BCCBTC', value: 'Bitcoin Cash / Bitcoin'}, + {key: 'BITTREX:ADABTC', value: 'Ada / Bitcoin'}, + {key: 'BITTREX:ARKBTC', value: 'Ark / Bitcoin'}, + {key: 'BITTREX:EMC2BTC', value: 'Einsteinium / Bitcoin'}, + {key: 'BITFINEX:IOTBTC', value: 'IOTA / Bitcoin'}, + {key: 'BITTREX:LSKBTC', value: 'Lisk / Bitcoin'}, + {key: 'BITTREX:NEOBTC', value: 'Neo / Bitcoin'}, + {key: 'BITTREX:OMGBTC', value: 'OmiseGO / Bitcoin'}, + {key: 'BITTREX:POWRBTC', value: 'PowerLedger / Bitcoin'}, + {key: 'BITTREX:STRATBTC', value: 'Stratis / Bitcoin'}, + {key: 'BITTREX:TRIGBTC', value: 'TRIG Token / Bitcoin'}, + {key: 'BITTREX:VTCBTC', value: 'Vertcoin / Bitcoin'}, + {key: 'BITTREX:XLMBTC', value: 'Lumen / Bitcoin'}, + {key: 'BITTREX:XRPBTC', value: 'Ripple / Bitcoin'}, // Misc - {value: 'BITTREX:BTCUSDT', label: 'Bitcoin / Tether USD'}, - {value: 'BITTREX:ETHUSDT', label: 'Ethereum / Tether USD'}, - {value: 'BITTREX:SNTETH', label: 'Status Network Token / Ethereum'}, - {value: 'BITTREX:BCCUSDT', label: 'Bitcoin Cash / Tether USD'}, - {value: 'BITTREX:NEOUSDT', label: 'Neo / Tether'}, + {key: 'BITTREX:BTCUSDT', value: 'Bitcoin / Tether USD'}, + {key: 'BITTREX:ETHUSDT', value: 'Ethereum / Tether USD'}, + {key: 'BITTREX:SNTETH', value: 'Status Network Token / Ethereum'}, + {key: 'BITTREX:BCCUSDT', value: 'Bitcoin Cash / Tether USD'}, + {key: 'BITTREX:NEOUSDT', value: 'Neo / Tether'}, ]; constructor(public translate: TranslateService) { @@ -80,7 +80,7 @@ export class TradingViewWidgetConfigComponent extends WidgetComponent { protected OnNewWidgetPrepared(widget: EditableWidget): void { widget.dimension.newData.interval = "D"; // 1 day - widget.dimension.newData.pair = this.pairs[0].value; + widget.dimension.newData.pair = this.pairs[0].key; } protected OnWidgetBeforeAdd(widget: EditableWidget): void { @@ -92,8 +92,8 @@ export class TradingViewWidgetConfigComponent extends WidgetComponent { } private setViewUrl(widget: EditableWidget) { - const pair = this.pairs.find(p => p.value === widget.dimension.newData.pair); - widget.dimension.newTitle = pair ? pair.label : null; + const pair = this.pairs.find(p => p.key === widget.dimension.newData.pair); + widget.dimension.newTitle = pair ? pair.value : null; widget.dimension.newUrl = window.location.origin + "/widgets/tradingview?pair=$pair&interval=$interval"; } } diff --git a/web/app/configs/widget/twitch/twitch.widget.component.html b/web/app/configs/widget/twitch/twitch.widget.component.html index aa80a26..58c439c 100644 --- a/web/app/configs/widget/twitch/twitch.widget.component.html +++ b/web/app/configs/widget/twitch/twitch.widget.component.html @@ -1,11 +1,14 @@ - + - + diff --git a/web/app/configs/widget/whiteboard/whiteboard.widget.component.html b/web/app/configs/widget/whiteboard/whiteboard.widget.component.html index 958ddaf..e14416f 100644 --- a/web/app/configs/widget/whiteboard/whiteboard.widget.component.html +++ b/web/app/configs/widget/whiteboard/whiteboard.widget.component.html @@ -1,18 +1,20 @@ - + - - + + diff --git a/web/app/configs/widget/whiteboard/whiteboard.widget.component.ts b/web/app/configs/widget/whiteboard/whiteboard.widget.component.ts index 8d3ce71..2988897 100644 --- a/web/app/configs/widget/whiteboard/whiteboard.widget.component.ts +++ b/web/app/configs/widget/whiteboard/whiteboard.widget.component.ts @@ -19,7 +19,6 @@ export class WhiteboardWidgetComponent extends WidgetComponent { } protected OnWidgetsDiscovered(widgets: EditableWidget[]): void { - console.log(widgets); for (const widget of widgets) { if (!widget.dimension.newUrl.startsWith("http://") && !widget.dimension.newUrl.startsWith("https://")) { const parsedUrl = url.parse(widget.url, true); diff --git a/web/app/configs/widget/youtube/youtube.widget.component.html b/web/app/configs/widget/youtube/youtube.widget.component.html index dec8bf7..3ae03ad 100644 --- a/web/app/configs/widget/youtube/youtube.widget.component.html +++ b/web/app/configs/widget/youtube/youtube.widget.component.html @@ -1,11 +1,14 @@ - + - + diff --git a/web/app/riot/riot-home/home.component.html b/web/app/element/element-home/home.component.html similarity index 74% rename from web/app/riot/riot-home/home.component.html rename to web/app/element/element-home/home.component.html index ec7f4d2..a316e5f 100644 --- a/web/app/riot/riot-home/home.component.html +++ b/web/app/element/element-home/home.component.html @@ -6,8 +6,6 @@
-
{{'Looking for your sticker packs?' | translate}} {{'Click here' | translate}}.
- @@ -29,11 +27,10 @@
- -
- -
-
+

{{category}}

+
+

{{'Sticker packs' | translate}}

+ {{'Manage your sticker packs' | translate}} {{'here' | translate}}
diff --git a/web/app/element/element-home/home.component.scss b/web/app/element/element-home/home.component.scss new file mode 100644 index 0000000..0d25859 --- /dev/null +++ b/web/app/element/element-home/home.component.scss @@ -0,0 +1,8 @@ +// component styles are encapsulated and only applied to their components +@import "../../../style/themes/themes"; + +@include themifyComponent() { + h3 { + font-size: 1.12em; + } +} diff --git a/web/app/riot/riot-home/home.component.ts b/web/app/element/element-home/home.component.ts similarity index 94% rename from web/app/riot/riot-home/home.component.ts rename to web/app/element/element-home/home.component.ts index 77808ac..12d0eb3 100644 --- a/web/app/riot/riot-home/home.component.ts +++ b/web/app/element/element-home/home.component.ts @@ -3,23 +3,16 @@ import { ActivatedRoute, Router } from "@angular/router"; import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service"; import * as _ from "lodash"; import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; -import { - FE_Integration, - FE_IntegrationRequirement, - FE_SimpleBot, -} from "../../shared/models/integration"; +import { FE_Integration, FE_IntegrationRequirement, FE_SimpleBot, } from "../../shared/models/integration"; import { IntegrationsRegistry } from "../../shared/registry/integrations.registry"; import { SessionStorage } from "../../shared/SessionStorage"; import { AdminApiService } from "../../shared/services/admin/admin-api.service"; import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service"; -import { - ConfigSimpleBotComponent, - SimpleBotConfigDialogContext, -} from "../../configs/simple-bot/simple-bot.component"; import { ToasterService } from "angular2-toaster"; import { StickerApiService } from "../../shared/services/integrations/sticker-api.service"; import { TranslateService } from "@ngx-translate/core"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { SimpleBotController } from "../../shared/helpers/SimpleBotController"; const CATEGORY_MAP = { Widgets: ["widget"], @@ -28,11 +21,11 @@ const CATEGORY_MAP = { }; @Component({ - selector: "my-riot-home", + selector: "my-element-home", templateUrl: "./home.component.html", styleUrls: ["./home.component.scss"], }) -export class RiotHomeComponent { +export class ElementHomeComponent { public isLoading = true; public isError = false; public errorMessage: string; @@ -173,11 +166,7 @@ export class RiotHomeComponent { public modifyIntegration(integration: FE_Integration) { if (!integration._isSupported) { console.log( - this.userId + - " tried to modify " + - integration.displayName + - " with error: " + - integration._notSupportedReason + this.userId + " tried to modify " + integration.displayName + " with error: " + integration._notSupportedReason ); this.translate .get( @@ -200,20 +189,10 @@ export class RiotHomeComponent { ); if (integration.category === "bot") { - const widgetConfigRef = this.modal.open(ConfigSimpleBotComponent, { - backdrop: "static", - size: "lg", - }); - const widgetConfigInterface = - widgetConfigRef.componentInstance as SimpleBotConfigDialogContext; - widgetConfigInterface.bot = integration; - widgetConfigInterface.roomId = this.roomId; + this.toggleBot(integration); } else { console.log( - "Navigating to edit screen for " + - integration.category + - " " + - integration.type + "Navigating to edit screen for " + integration.category + " " + integration.type ); this.router.navigate( ["riot-app", integration.category, integration.type], @@ -222,6 +201,11 @@ export class RiotHomeComponent { } } + public toggleBot(bot: FE_SimpleBot) { + const controller = new SimpleBotController(bot, this.scalar, this.integrationsApi, this.toaster, this.translate); + controller.toggle(this.roomId); + } + private prepareIntegrations() { this.scalar .isRoomEncrypted(this.roomId) @@ -342,6 +326,8 @@ export class RiotHomeComponent { } private async updateIntegrationState(integration: FE_Integration) { + integration._isUpdating = false; + if (!integration.isOnline) { integration._isSupported = false; this.translate diff --git a/web/app/riot/riot.component.html b/web/app/element/element.component.html similarity index 85% rename from web/app/riot/riot.component.html rename to web/app/element/element.component.html index 3799567..70020fd 100644 --- a/web/app/riot/riot.component.html +++ b/web/app/element/element.component.html @@ -1,7 +1,7 @@
- +
@@ -9,4 +9,4 @@
-
\ No newline at end of file +
diff --git a/web/app/riot/riot.component.scss b/web/app/element/element.component.scss similarity index 77% rename from web/app/riot/riot.component.scss rename to web/app/element/element.component.scss index dbc8212..f566e8c 100644 --- a/web/app/riot/riot.component.scss +++ b/web/app/element/element.component.scss @@ -1,4 +1,4 @@ // component styles are encapsulated and only applied to their components .page-content { - padding: 30px; -} \ No newline at end of file + padding: 0 32px 32px; +} diff --git a/web/app/riot/riot.component.ts b/web/app/element/element.component.ts similarity index 59% rename from web/app/riot/riot.component.ts rename to web/app/element/element.component.ts index bf255ad..7434373 100644 --- a/web/app/riot/riot.component.ts +++ b/web/app/element/element.component.ts @@ -2,11 +2,11 @@ import { Component } from "@angular/core"; import { SessionStorage } from "../shared/SessionStorage"; @Component({ - selector: "my-riot", - templateUrl: "./riot.component.html", - styleUrls: ["./riot.component.scss"], + selector: "my-element", + templateUrl: "./element.component.html", + styleUrls: ["./element.component.scss"], }) -export class RiotComponent { +export class ElementComponent { constructor() {} public isAdmin(): boolean { diff --git a/web/app/riot/scalar-close/scalar-close.component.html b/web/app/element/scalar-close/scalar-close.component.html similarity index 50% rename from web/app/riot/scalar-close/scalar-close.component.html rename to web/app/element/scalar-close/scalar-close.component.html index 355965e..d6bb413 100644 --- a/web/app/riot/scalar-close/scalar-close.component.html +++ b/web/app/element/scalar-close/scalar-close.component.html @@ -1,3 +1,3 @@ - - \ No newline at end of file +
+ diff --git a/web/app/element/scalar-close/scalar-close.component.scss b/web/app/element/scalar-close/scalar-close.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/riot/scalar-close/scalar-close.component.ts b/web/app/element/scalar-close/scalar-close.component.ts similarity index 90% rename from web/app/riot/scalar-close/scalar-close.component.ts rename to web/app/element/scalar-close/scalar-close.component.ts index 042e33c..93d882e 100644 --- a/web/app/riot/scalar-close/scalar-close.component.ts +++ b/web/app/element/scalar-close/scalar-close.component.ts @@ -10,7 +10,7 @@ export class ScalarCloseComponent { constructor(private scalar: ScalarClientApiService) {} public closeScalar() { - console.log("Closing scalar..."); + console.log("Closing Dimension..."); this.scalar.close(); } } diff --git a/web/app/elements/field/field.component.html b/web/app/elements/field/field.component.html new file mode 100644 index 0000000..d4cb74e --- /dev/null +++ b/web/app/elements/field/field.component.html @@ -0,0 +1,22 @@ +
+ + + +
diff --git a/web/app/elements/field/field.component.scss b/web/app/elements/field/field.component.scss new file mode 100644 index 0000000..0e4f4e2 --- /dev/null +++ b/web/app/elements/field/field.component.scss @@ -0,0 +1,93 @@ +@import "../../../style/themes/themes"; + +// CSS largely copied from https://github.com/matrix-org/matrix-react-sdk/blob/9db0ebb7f5479f70e8f637b76f0a4470de591f34/res/css/views/elements/_Field.scss +// to match Element styling. + +@include themifyComponent() { + .field { + display: flex; + flex: 1; + min-width: 0; + position: relative; + margin: 1em 0; + border-radius: 4px; + transition: border-color 0.25s; + border: 1px solid themed(formControlBorderColor); + + input, select, textarea { + font-family: inherit; + font-weight: normal; + font-size: 14px; + border: none; + border-radius: 4px; + padding: 8px 9px; + color: themed(formControlFgColor); + background-color: themed(formControlBgColor); + flex: 1; + min-width: 0; + + &:focus { + outline: 0; + } + + &::placeholder { + transition: color 0.25s ease-in 0s; + color: transparent; + } + + &:placeholder-shown:focus::placeholder { + transition: color 0.25s ease-in 0.1s; + color: themed(formControlPlaceholderColor); + } + } + + &:focus-within { + border-color: themed(accentAlt); + } + + label { + transition: + font-size 0.25s ease-out 0.1s, + color 0.25s ease-out 0.1s, + transform 0.25s ease-out 0.1s, + background-color 0.25s ease-out 0.1s; + background-color: transparent; + font-size: 14px; + transform: translateY(0); + position: absolute; + left: 0; + margin: 7px 8px; + padding: 2px; + pointer-events: none; // Allow clicks to fall through to the input + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: calc(100% - 20px); // 100% of parent minus margin and padding + } + + select + label, + input:focus + label, + input:not(:placeholder-shown) + label { + transition: + font-size 0.25s ease-out 0s, + color 0.25s ease-out 0s, + transform 0.25s ease-out 0s, + background-color 0.25s ease-out 0s; + font-size: 10px; + transform: translateY(-13px); + padding: 0 2px; + background-color: themed(formControlBgColor); + pointer-events: initial; + } + + input:focus + label { + color: themed(accentAlt); + } + + input:disabled, + input:disabled + label { + background-color: themed(formControlBgColor); + color: themed(formControlPlaceholderColor); + } + } +} diff --git a/web/app/elements/field/field.component.ts b/web/app/elements/field/field.component.ts new file mode 100644 index 0000000..e07934a --- /dev/null +++ b/web/app/elements/field/field.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, forwardRef, Input, Output } from "@angular/core"; + +@Component({ + selector: "my-field", + templateUrl: "./field.component.html", + styleUrls: ["./field.component.scss"], +}) +export class FieldComponent{ + @Input() type = "text"; + @Input() value: string; + @Output() valueChange = new EventEmitter(); + @Input() placeholder?: string; + @Input() label: string; + @Input() disabled?: boolean; + @Input() maxlength?: number; + @Input() asSelect?: boolean; + @Input() selectOptions?: ({key: string, value?: string}|string)[]; + + public id = `${new Date().getTime()}-field-${Math.random()}`; + + public get options(): {key: string, value: string}[] { + return this.selectOptions.map(p => { + if (typeof(p) === 'string') { + return {key: p, value: p}; + } + return {key: p.key, value: p.value ?? p.key}; + }); + } + + public onValueChange() { + this.valueChange.emit(this.value); + } +} diff --git a/web/app/elements/spinner/spinner.component.html b/web/app/elements/spinner/spinner.component.html index 86c0ae5..3cbb393 100644 --- a/web/app/elements/spinner/spinner.component.html +++ b/web/app/elements/spinner/spinner.component.html @@ -1,6 +1,5 @@ -
-
-
-
-
-
\ No newline at end of file + + + + + diff --git a/web/app/elements/spinner/spinner.component.scss b/web/app/elements/spinner/spinner.component.scss index 48f2e7b..2186c9f 100644 --- a/web/app/elements/spinner/spinner.component.scss +++ b/web/app/elements/spinner/spinner.component.scss @@ -1,4 +1,49 @@ -// component styles are encapsulated and only applied to their components -@import "../../../../node_modules/spinkit/spinkit.min.css"; +@import "../../../style/themes/themes"; -// see app.scss for the spinner color stuff \ No newline at end of file +@include themifyComponent() { + .container { + position: relative; + } + + .container .spinner { + height: 30px; + width: 30px; + animation: rotate 2s linear infinite; + transform-origin: center center; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + } + + .container .spinner .path { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + animation: dash 1.5s ease-in-out infinite; + stroke-linecap: round; + stroke: themed(defaultFgColor); + } + + @keyframes rotate { + 100% { + transform: rotate(360deg); + } + } + + @keyframes dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -35px; + } + 100% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -124px; + } + } +} diff --git a/web/app/home/home.component.html b/web/app/home/home.component.html index b3f4c61..b4abc72 100644 --- a/web/app/home/home.component.html +++ b/web/app/home/home.component.html @@ -161,6 +161,10 @@ Slack
+
+ + GitHub +
Webhooks diff --git a/web/app/integration-bag/integration-bag.component.html b/web/app/integration-bag/integration-bag.component.html index 54a6e02..c2b4a01 100644 --- a/web/app/integration-bag/integration-bag.component.html +++ b/web/app/integration-bag/integration-bag.component.html @@ -3,7 +3,9 @@
{{integration.displayName | translate}}
{{ integration.description | translate }}
-
+
+ +
diff --git a/web/app/integration-bag/integration-bag.component.scss b/web/app/integration-bag/integration-bag.component.scss index 57aa841..7689600 100644 --- a/web/app/integration-bag/integration-bag.component.scss +++ b/web/app/integration-bag/integration-bag.component.scss @@ -12,46 +12,37 @@ .integration { border: 1px solid themed(integrationBorderColor); - margin: 7px; - padding: 6px; + margin-right: 16px; + margin-bottom: 16px; + padding: 12px; width: calc(325px - 14px); position: relative; cursor: pointer; - } + border-radius: 8px; - .integration .integration-arrow { - display: none; - position: absolute; - top: calc(50% - 12px); // icon happens to be 24px tall - right: 1px; - color: #bbb; - } - - .integration:hover { - box-shadow: 0 0 5px 5px themed(integrationShadowColor); - } - - .integration:hover .integration-arrow { - display: block; + .bot-toggle { + position: absolute; + top: 6px; + right: 6px; + } } .integration .integration-avatar { - width: 50px; - height: 50px; + width: 52px; + height: 52px; float: left; } .integration .integration-name { display: inline-block; - font-size: 1.1em; - font-weight: 100; - padding-left: 10px; + font-weight: 600; + padding-left: 12px; } .integration .integration-description { display: block; font-size: 0.8em; - margin-left: 60px; + margin-left: 65px; margin-right: 5px; color: themed(integrationDescriptionColor); } @@ -61,4 +52,4 @@ top: 5px; right: 6px; } -} \ No newline at end of file +} diff --git a/web/app/integration-bag/integration-bag.component.ts b/web/app/integration-bag/integration-bag.component.ts index 2227edb..4d4cb03 100644 --- a/web/app/integration-bag/integration-bag.component.ts +++ b/web/app/integration-bag/integration-bag.component.ts @@ -1,12 +1,4 @@ -import { - Component, - EventEmitter, - Input, - OnChanges, - Output, - SimpleChange, - SimpleChanges, -} from "@angular/core"; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges, } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; import { FE_Integration } from "../shared/models/integration"; import { MediaService } from "../shared/services/media.service"; diff --git a/web/app/page-header/page-header.component.html b/web/app/page-header/page-header.component.html index 6933952..45bc53a 100644 --- a/web/app/page-header/page-header.component.html +++ b/web/app/page-header/page-header.component.html @@ -1,11 +1,11 @@ -
-
-
-

{{ pageName | translate }}

- -
-
- +
+
+
+ {{ 'Browse integrations' | translate }}
+

{{ pageName | translate }}

+
+
+
diff --git a/web/app/page-header/page-header.component.scss b/web/app/page-header/page-header.component.scss index 8c45ee7..0376a7e 100644 --- a/web/app/page-header/page-header.component.scss +++ b/web/app/page-header/page-header.component.scss @@ -2,30 +2,23 @@ @import "../../style/themes/themes"; @include themifyComponent() { - .wrapper { - width: 100%; - padding-top: 10px; - background: linear-gradient(to right, themed(headerGradientStartColor), themed(headerGradientEndColor)); - } - .header { width: 100%; - border-top: 1px solid themed(headerBorderColor); - border-bottom: 1px solid themed(headerBorderColor); + padding: 32px; background-color: themed(headerBgColor); } .header .title { margin: 0; width: 83.333333%; // col-sm-10 - padding: 15px; display: inline-block; } .header .title .pageName { - font-weight: 100; + font-weight: 600; padding: 0; margin: 0; + font-size: 1.5em; } .header .quickAction { @@ -37,4 +30,16 @@ width: 15.666667%; // col-sm-2 position: relative; } -} \ No newline at end of file + + .back-btn { + cursor: pointer; + margin-bottom: 16px; + } + + .back-btn > span:not(.dim-icon) { + margin-left: 4px; + line-height: 1em; + font-size: 0.9em; + } +} + diff --git a/web/app/page-header/page-header.component.ts b/web/app/page-header/page-header.component.ts index d51d0b3..7a57821 100644 --- a/web/app/page-header/page-header.component.ts +++ b/web/app/page-header/page-header.component.ts @@ -1,10 +1,5 @@ import { Component } from "@angular/core"; -import { - ActivatedRoute, - NavigationEnd, - PRIMARY_OUTLET, - Router, -} from "@angular/router"; +import { ActivatedRoute, NavigationEnd, PRIMARY_OUTLET, Router, } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { filter } from "rxjs/operators"; @@ -15,38 +10,43 @@ import { filter } from "rxjs/operators"; }) export class PageHeaderComponent { public pageName: string; + public isHome = true; constructor( private router: Router, private activatedRoute: ActivatedRoute, public translate: TranslateService ) { - this.translate = translate; this.router.events .pipe(filter((ev) => ev instanceof NavigationEnd)) .subscribe((ev) => { let currentRoute = this.activatedRoute.root; let url = ""; const event = ev as NavigationEnd; + let routeCount = 0; while (currentRoute.children.length > 0) { const children = currentRoute.children; children.forEach((route) => { - if (route.snapshot.data["breadcrumb"]) { - this.translate - .get(route.snapshot.data["breadcrumb"]) - .subscribe((res: string) => { - route.snapshot.data["breadcrumb"] = res; - }); + const breadcrumb = route.snapshot.data["breadcrumb"]; + if (typeof(breadcrumb) === 'string' && !route.snapshot.data['#translated']) { + this.translate.get(breadcrumb).subscribe((res: string) => { + route.snapshot.data["breadcrumb"] = res; + route.snapshot.data['#translated'] = true; + }); } currentRoute = route; + routeCount++; url += "/" + route.snapshot.url.map((s) => s.path).join("/"); if (route.outlet !== PRIMARY_OUTLET) return; if (!route.routeConfig || !route.routeConfig.data) return; - if (url === event.urlAfterRedirects.split("?")[0]) + if (url === event.urlAfterRedirects.split("?")[0]) { this.pageName = route.snapshot.data.name; + } }); } + + this.isHome = routeCount <= 2; // Path of `./riot-app` (2 segments) }); } } diff --git a/web/app/riot/riot-home/home.component.scss b/web/app/riot/riot-home/home.component.scss deleted file mode 100644 index 9973ff8..0000000 --- a/web/app/riot/riot-home/home.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -// component styles are encapsulated and only applied to their components -@import "../../../style/themes/themes"; - -@include themifyComponent() { - .stickerpacks { - position: relative; - top: -15px; - padding-left: 10px; - font-size: 0.8em; - color: themed(stickerpacksNotifFgColor); - } -} \ No newline at end of file diff --git a/web/app/configs/simple-bot/simple-bot.component.ts b/web/app/shared/helpers/SimpleBotController.ts similarity index 50% rename from web/app/configs/simple-bot/simple-bot.component.ts rename to web/app/shared/helpers/SimpleBotController.ts index 5cca6c5..57cebfa 100644 --- a/web/app/configs/simple-bot/simple-bot.component.ts +++ b/web/app/shared/helpers/SimpleBotController.ts @@ -1,47 +1,31 @@ -import { Component, OnInit } from "@angular/core"; -import { FE_SimpleBot } from "../../shared/models/integration"; +import { FE_SimpleBot } from "../models/integration"; +import { ScalarClientApiService } from "../services/scalar/scalar-client-api.service"; import { ToasterService } from "angular2-toaster"; -import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service"; -import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service"; import { TranslateService } from "@ngx-translate/core"; -import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; +import { IntegrationsApiService } from "../services/integrations/integrations-api.service"; -export interface SimpleBotConfigDialogContext { - bot: FE_SimpleBot; - roomId: string; -} - -@Component({ - templateUrl: "simple-bot.component.html", - styleUrls: ["simple-bot.component.scss"], -}) -export class ConfigSimpleBotComponent implements OnInit { - - public bot: FE_SimpleBot; - - private roomId: string; - - constructor(public modal: NgbActiveModal, - private toaster: ToasterService, +export class SimpleBotController { + constructor( + public readonly bot: FE_SimpleBot, private scalar: ScalarClientApiService, private integrationsApi: IntegrationsApiService, - public translate: TranslateService) { - this.translate = translate; + private toaster: ToasterService, + private translate: TranslateService, + ) { } - ngOnInit() { - this.bot._isUpdating = false; - } - - public toggle() { - let promise: Promise = Promise.resolve(); + public async toggle(roomId: string): Promise { + let prom1: Promise; if (!this.bot._inRoom) { - promise = this.scalar.inviteUser(this.roomId, this.bot.userId); - } else promise = this.integrationsApi.removeIntegration(this.bot.category, this.bot.type, this.roomId); + prom1 = this.scalar.inviteUser(roomId, this.bot.userId); + } else { + prom1 = this.integrationsApi.removeIntegration(this.bot.category, this.bot.type, roomId); + } this.bot._inRoom = !this.bot._inRoom; this.bot._isUpdating = true; - promise.then(() => { + try { + await prom1; this.bot._isUpdating = false; if (this.bot._inRoom) { this.translate.get('was invited to the room').subscribe((res: string) => { @@ -52,7 +36,7 @@ export class ConfigSimpleBotComponent implements OnInit { this.toaster.pop("success", this.bot.displayName + res); }); } - }).catch(err => { + } catch (err) { this.bot._inRoom = !this.bot._inRoom; // revert the status change this.bot._isUpdating = false; console.error(err); @@ -61,9 +45,9 @@ export class ConfigSimpleBotComponent implements OnInit { if (err.json) errorMessage = err.json().error; if (err.response && err.response.error) errorMessage = err.response.error.message; if (!errorMessage) errorMessage = ""; - this.translate.get('Could not update integration status').subscribe((res: string) => { - this.toaster.pop("error", res); + this.translate.get('Could not update integration status: ').subscribe((res: string) => { + this.toaster.pop("error", res + errorMessage); }); - }); + } } } diff --git a/web/app/shared/models/hookshot_github.ts b/web/app/shared/models/hookshot_github.ts new file mode 100644 index 0000000..1680928 --- /dev/null +++ b/web/app/shared/models/hookshot_github.ts @@ -0,0 +1,39 @@ +export interface FE_HookshotGithubBridge { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +export interface FE_HookshotGithubConnection { + config: { + org: string; + repo: string; + commandPrefix?: string; + }; +} + +export interface FE_HookshotGithubRepo { + name: string; + owner: string; + fullName: string; + avatarUrl: string; + description: string; +} + +export interface FE_HookshotGithubAuthUrls { + userUrl: string; + orgUrl: string; +} + +export interface FE_HookshotGithubOrg { + name: string; + avatarUrl: string; +} + +export interface FE_HookshotGithubOrgReposDto { + organization: FE_HookshotGithubOrg; + repositories: FE_HookshotGithubRepo[]; + changeSelectionUrl?: string; +} diff --git a/web/app/shared/models/hookshot_jira.ts b/web/app/shared/models/hookshot_jira.ts new file mode 100644 index 0000000..d439f7f --- /dev/null +++ b/web/app/shared/models/hookshot_jira.ts @@ -0,0 +1,25 @@ +export interface FE_HookshotJiraBridge { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +export interface FE_HookshotJiraConnection { + config: { + url: string; + commandPrefix?: string; + }; +} + +export interface FE_HookshotJiraInstance { + name: string; + url: string; +} + +export interface FE_HookshotJiraProject { + key: string; + name: string; + url: string; +} diff --git a/web/app/shared/models/hookshot_webhook.ts b/web/app/shared/models/hookshot_webhook.ts new file mode 100644 index 0000000..4f8c2cf --- /dev/null +++ b/web/app/shared/models/hookshot_webhook.ts @@ -0,0 +1,15 @@ +export interface FE_HookshotWebhookBridge { + id: number; + upstreamId?: number; + provisionUrl?: string; + sharedSecret?: string; + isEnabled: boolean; +} + +export interface FE_HookshotWebhookConnection { + id: string; + config: { + name?: string; + url: string; + }; +} diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 6f60fd3..f10d653 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { - WIDGET_CUSTOM, WIDGET_BIGBLUEBUTTON, + WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, @@ -11,8 +11,8 @@ import { WIDGET_STICKER_PICKER, WIDGET_TRADINGVIEW, WIDGET_TWITCH, - WIDGET_YOUTUBE, - WIDGET_WHITEBOARD + WIDGET_WHITEBOARD, + WIDGET_YOUTUBE } from "../models/widget"; import { FE_Integration } from "../models/integration"; @@ -31,6 +31,9 @@ export class IntegrationsRegistry { "telegram": {}, "webhooks": {}, "slack": {}, + "hookshot_github": {}, + "hookshot_jira": {}, + "hookshot_webhook": {}, }, "widget": { "custom": { diff --git a/web/app/shared/services/admin/admin-hookshot-github-api.service.ts b/web/app/shared/services/admin/admin-hookshot-github-api.service.ts new file mode 100644 index 0000000..4290023 --- /dev/null +++ b/web/app/shared/services/admin/admin-hookshot-github-api.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { FE_Upstream } from "../../models/admin-responses"; +import { HttpClient } from "@angular/common/http"; +import { FE_HookshotGithubBridge } from "../../models/hookshot_github"; + +@Injectable() +export class AdminHookshotGithubApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public getBridges(): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/github/all").toPromise(); + } + + public getBridge(bridgeId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/github/" + bridgeId).toPromise(); + } + + public newFromUpstream(upstream: FE_Upstream): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/github/new/upstream", {upstreamId: upstream.id}).toPromise(); + } + + public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/github/new/selfhosted", { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } + + public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/github/" + bridgeId, { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } +} diff --git a/web/app/shared/services/admin/admin-hookshot-jira-api.service.ts b/web/app/shared/services/admin/admin-hookshot-jira-api.service.ts new file mode 100644 index 0000000..e9ffb57 --- /dev/null +++ b/web/app/shared/services/admin/admin-hookshot-jira-api.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { FE_Upstream } from "../../models/admin-responses"; +import { HttpClient } from "@angular/common/http"; +import { FE_HookshotJiraBridge } from "../../models/hookshot_jira"; + +@Injectable() +export class AdminHookshotJiraApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public getBridges(): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/jira/all").toPromise(); + } + + public getBridge(bridgeId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/jira/" + bridgeId).toPromise(); + } + + public newFromUpstream(upstream: FE_Upstream): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/jira/new/upstream", {upstreamId: upstream.id}).toPromise(); + } + + public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/jira/new/selfhosted", { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } + + public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/jira/" + bridgeId, { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } +} diff --git a/web/app/shared/services/admin/admin-hookshot-webhook-api.service.ts b/web/app/shared/services/admin/admin-hookshot-webhook-api.service.ts new file mode 100644 index 0000000..df475ac --- /dev/null +++ b/web/app/shared/services/admin/admin-hookshot-webhook-api.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { FE_Upstream } from "../../models/admin-responses"; +import { HttpClient } from "@angular/common/http"; +import { FE_HookshotWebhookBridge } from "../../models/hookshot_webhook"; + +@Injectable() +export class AdminHookshotWebhookApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public getBridges(): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/webhook/all").toPromise(); + } + + public getBridge(bridgeId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/hookshot/webhook/" + bridgeId).toPromise(); + } + + public newFromUpstream(upstream: FE_Upstream): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/webhook/new/upstream", {upstreamId: upstream.id}).toPromise(); + } + + public newSelfhosted(provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/webhook/new/selfhosted", { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } + + public updateSelfhosted(bridgeId: number, provisionUrl: string, sharedSecret: string): Promise { + return this.authedPost("/api/v1/dimension/admin/hookshot/webhook/" + bridgeId, { + provisionUrl: provisionUrl, + sharedSecret: sharedSecret, + }).toPromise(); + } +} diff --git a/web/app/shared/services/integrations/bigbluebutton-api.service.ts b/web/app/shared/services/integrations/bigbluebutton-api.service.ts index 028fe13..05e7664 100644 --- a/web/app/shared/services/integrations/bigbluebutton-api.service.ts +++ b/web/app/shared/services/integrations/bigbluebutton-api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { AuthedApi } from "../authed-api"; -import { FE_BigBlueButtonJoin, FE_BigBlueButtonCreateAndJoinMeeting } from "../../models/integration" +import { FE_BigBlueButtonCreateAndJoinMeeting, FE_BigBlueButtonJoin } from "../../models/integration" import { HttpClient } from "@angular/common/http"; import { ApiError } from "../../../../../src/api/ApiError"; @@ -21,4 +21,4 @@ export class BigBlueButtonApiService extends AuthedApi { ).toPromise(); } -} \ No newline at end of file +} diff --git a/web/app/shared/services/integrations/hookshot-github-api.service.ts b/web/app/shared/services/integrations/hookshot-github-api.service.ts new file mode 100644 index 0000000..33676fe --- /dev/null +++ b/web/app/shared/services/integrations/hookshot-github-api.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { HttpClient } from "@angular/common/http"; +import { + FE_HookshotGithubAuthUrls, + FE_HookshotGithubConnection, + FE_HookshotGithubOrgReposDto, +} from "../../models/hookshot_github"; + +@Injectable() +export class HookshotGithubApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public bridgeRoom(roomId: string, orgId: string, repoId: string): Promise { + return this.authedPost("/api/v1/dimension/hookshot/github/room/" + roomId + "/connect", { + orgId, + repoId, + }).toPromise(); + } + + public unbridgeRoom(roomId: string): Promise { + return this.authedDelete("/api/v1/dimension/hookshot/github/room/" + roomId + "/connections/all").toPromise(); + } + + public getAuthUrls(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/github/auth").toPromise(); + } + + public getInstalledLocations(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/github/locations").toPromise().then(r => r['locations']); + } +} diff --git a/web/app/shared/services/integrations/hookshot-jira-api.service.ts b/web/app/shared/services/integrations/hookshot-jira-api.service.ts new file mode 100644 index 0000000..ad45079 --- /dev/null +++ b/web/app/shared/services/integrations/hookshot-jira-api.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { HttpClient } from "@angular/common/http"; +import { FE_HookshotJiraConnection, FE_HookshotJiraInstance, FE_HookshotJiraProject } from "../../models/hookshot_jira"; + +@Injectable() +export class HookshotJiraApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public getProjects(instanceName: string): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/instance/" + instanceName + "/projects").toPromise().then(r => r['projects']); + } + + public getInstances(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/instances").toPromise().then(r => r['instances']); + } + + public getAuthUrl(): Promise { + return this.authedGet("/api/v1/dimension/hookshot/jira/auth").toPromise().then(r => r['authUrl']); + } + + public bridgeRoom(roomId: string, instanceName: string, projectKey: string): Promise { + return this.authedPost("/api/v1/dimension/hookshot/jira/room/" + roomId + "/connect", { + instanceName: instanceName, + projectKey: projectKey, + }).toPromise(); + } + + public unbridgeRoom(roomId: string): Promise { + return this.authedDelete("/api/v1/dimension/hookshot/jira/room/" + roomId + "/connections/all").toPromise(); + } +} diff --git a/web/app/shared/services/integrations/hookshot-webhook-api.service.ts b/web/app/shared/services/integrations/hookshot-webhook-api.service.ts new file mode 100644 index 0000000..5802149 --- /dev/null +++ b/web/app/shared/services/integrations/hookshot-webhook-api.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { HttpClient } from "@angular/common/http"; +import { FE_HookshotWebhookConnection } from "../../models/hookshot_webhook"; + +@Injectable() +export class HookshotWebhookApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public createWebhook(roomId: string, name: string): Promise { + return this.authedPost("/api/v1/dimension/hookshot/webhook/room/" + roomId + "/connect", { + name, + }).toPromise(); + } + + public deleteWebhook(roomId: string, webhookId: string): Promise { + return this.authedDelete("/api/v1/dimension/hookshot/webhook/room/" + roomId + "/connection/" + encodeURIComponent(webhookId) + "/disconnect").toPromise(); + } + +} diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts index e218310..ae69e28 100644 --- a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts @@ -5,10 +5,7 @@ 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_BigBlueButtonCreateAndJoinMeeting, - FE_BigBlueButtonJoin, -} from "../../shared/models/integration"; +import { FE_BigBlueButtonCreateAndJoinMeeting, FE_BigBlueButtonJoin, } from "../../shared/models/integration"; import { TranslateService } from "@ngx-translate/core"; @Component({ @@ -164,8 +161,6 @@ export class BigBlueButtonWidgetWrapperComponent this.meetingPassword ) .then((response) => { - console.log("The response"); - console.log(response); if ("errorCode" in response) { // This is an instance of ApiError if (response.errorCode === "UNKNOWN_MEETING_ID") { diff --git a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss index 6ccaaf2..9396b83 100644 --- a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss +++ b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss @@ -51,13 +51,13 @@ margin-left: 3px; .title { - font-weight: 700; + font-weight: bold; color: themed(stickerPickerTitleColor); } .license { font-size: 0.6em; - font-weight: 300; + font-weight: bold; color: themed(stickerPickerLicenseColor); margin-top: 3px; float: right; @@ -69,7 +69,7 @@ .author { font-size: 0.6em; - font-weight: 300; + font-weight: bold; color: themed(stickerPickerAuthorColor); display: block; @@ -122,4 +122,4 @@ } } } -} \ No newline at end of file +} diff --git a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts index 312def6..039d726 100644 --- a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts +++ b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts @@ -1,37 +1,15 @@ -import { - animate, - state, - style, - transition, - trigger, -} from "@angular/animations"; -import { - AfterViewInit, - ChangeDetectorRef, - Component, - OnDestroy, - OnInit, -} from "@angular/core"; +import { animate, state, style, transition, trigger, } from "@angular/animations"; +import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { CapableWidget, WIDGET_API_VERSION_OPENID } from "../capable-widget"; import { fromEvent } from "rxjs"; -import { - distinctUntilChanged, - filter, - map, - pairwise, - share, - throttleTime, -} from "rxjs/operators"; +import { distinctUntilChanged, filter, map, pairwise, share, throttleTime, } from "rxjs/operators"; import { Subscription } from "rxjs/Subscription"; import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"; import { StickerApiService } from "../../shared/services/integrations/sticker-api.service"; import { SessionStorage } from "../../shared/SessionStorage"; import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; -import { - FE_Sticker, - FE_UserStickerPack, -} from "../../shared/models/integration"; +import { FE_Sticker, FE_UserStickerPack, } from "../../shared/models/integration"; import { MediaService } from "../../shared/services/media.service"; import { WIDGET_STICKER_PICKER } from "../../shared/models/widget"; diff --git a/web/assets/fonts/Inter-Black.ttf b/web/assets/fonts/Inter-Black.ttf new file mode 100644 index 0000000..ec7bfb6 Binary files /dev/null and b/web/assets/fonts/Inter-Black.ttf differ diff --git a/web/assets/fonts/Inter-Bold.ttf b/web/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..acaa7e2 Binary files /dev/null and b/web/assets/fonts/Inter-Bold.ttf differ diff --git a/web/assets/fonts/Inter-ExtraBold.ttf b/web/assets/fonts/Inter-ExtraBold.ttf new file mode 100644 index 0000000..d892efe Binary files /dev/null and b/web/assets/fonts/Inter-ExtraBold.ttf differ diff --git a/web/assets/fonts/Inter-ExtraLight.ttf b/web/assets/fonts/Inter-ExtraLight.ttf new file mode 100644 index 0000000..b4308fc Binary files /dev/null and b/web/assets/fonts/Inter-ExtraLight.ttf differ diff --git a/web/assets/fonts/Inter-Light.ttf b/web/assets/fonts/Inter-Light.ttf new file mode 100644 index 0000000..025fc31 Binary files /dev/null and b/web/assets/fonts/Inter-Light.ttf differ diff --git a/web/assets/fonts/Inter-Medium.ttf b/web/assets/fonts/Inter-Medium.ttf new file mode 100644 index 0000000..5b37f99 Binary files /dev/null and b/web/assets/fonts/Inter-Medium.ttf differ diff --git a/web/assets/fonts/Inter-Regular.ttf b/web/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..6b3ea5d Binary files /dev/null and b/web/assets/fonts/Inter-Regular.ttf differ diff --git a/web/assets/fonts/Inter-SemiBold.ttf b/web/assets/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000..bb823af Binary files /dev/null and b/web/assets/fonts/Inter-SemiBold.ttf differ diff --git a/web/assets/fonts/Inter-Thin.ttf b/web/assets/fonts/Inter-Thin.ttf new file mode 100644 index 0000000..fc8116d Binary files /dev/null and b/web/assets/fonts/Inter-Thin.ttf differ diff --git a/web/assets/fonts/inter.scss b/web/assets/fonts/inter.scss new file mode 100644 index 0000000..77171d4 --- /dev/null +++ b/web/assets/fonts/inter.scss @@ -0,0 +1,72 @@ +@font-face { + font-family: 'Inter'; + src: local('Inter Bold'), local('Inter-Bold'), + url('./Inter-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter ExtraBold'), local('Inter-ExtraBold'), + url('./Inter-ExtraBold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter Black'), local('Inter-Black'), + url('./Inter-Black.ttf') format('truetype'); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter Light'), local('Inter-Light'), + url('./Inter-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter ExtraLight'), local('Inter-ExtraLight'), + url('./Inter-ExtraLight.ttf') format('truetype'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter Medium'), local('Inter-Medium'), + url('./Inter-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter Regular'), local('Inter-Regular'), + url('./Inter-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter SemiBold'), local('Inter-SemiBold'), + url('./Inter-SemiBold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: 'Inter'; + src: local('Inter Thin'), local('Inter-Thin'), + url('./Inter-Thin.ttf') format('truetype'); + font-weight: 100; + font-style: normal; +} + diff --git a/web/assets/fonts/opensans100-roboto300.scss b/web/assets/fonts/opensans100-roboto300.scss deleted file mode 100644 index a42b168..0000000 --- a/web/assets/fonts/opensans100-roboto300.scss +++ /dev/null @@ -1,6 +0,0 @@ -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 300; - src: local('Roboto Light'), local('Roboto-Light'), url('./opensans100-roboto300.ttf') format('truetype'); -} diff --git a/web/assets/fonts/opensans100-roboto300.ttf b/web/assets/fonts/opensans100-roboto300.ttf deleted file mode 100644 index 57a24e8..0000000 Binary files a/web/assets/fonts/opensans100-roboto300.ttf and /dev/null differ diff --git a/web/assets/i18n/en.json b/web/assets/i18n/en.json index 2f05ffc..18c502a 100644 --- a/web/assets/i18n/en.json +++ b/web/assets/i18n/en.json @@ -1,467 +1,512 @@ { - "Dashboard": "Dashboard", - "Widgets": "Widgets", - "go-neb": "go-neb", - "Custom Bots": "Custom Bots", - "Bridges": "Bridges", - "Sticker Packs": "Sticker Packs", - "Terms of Service": "Terms of Service", - "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.": "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.", - "Name": "Name", - "Description": "Description", - "Actions": "Actions", - "No bridges.": "No bridges.", - "matrix-appservice-gitter": "matrix-appservice-gitter", - "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.": "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.", - "No bridge configurations.": "No bridge configurations.", - "Add matrix.org's bridge": "Add matrix.org's bridge", - "Add self-hosted bridge": "Add self-hosted bridge", - "self-hosted Gitter bridge": "self-hosted Gitter bridge ", - "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", - "Provisioning URL": "Provisioning URL", - "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.", - "Save": "Save", - "Cancel": "Cancel", - "Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge ", - "Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have", - "provisioning": "provisioning", - "enabled in the configuration.": "enabled in the configuration.", - "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.", - "matrix-appservice-irc": "matrix-appservice-irc", - "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.", - "Enabled Networks": "Enabled Networks", - "This bridge is offline or unavailable.": "This bridge is offline or unavailable.", - "Network": "Network", - "Enabled": "Enabled", - "Close": "Close", - "self-hosted Slack bridge": "self-hosted Slack bridge ", - "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", - "matrix-appservice-slack": "matrix-appservice-slack", - "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.", - "self-hosted Telegram bridge": "self-hosted Telegram bridge ", - "Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have", - "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.", - "Shared Secret": "Shared Secret", - "The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.", - "Promote Telegram Puppeting": "Promote Telegram Puppeting", - "If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.", - "Promote Matrix Puppeting": "Promote Matrix Puppeting", - "If enabled, Dimension will recommend that users log in to their Matrix accounts.": "If enabled, Dimension will recommend that users log in to their Matrix accounts.", - "mautrix-telegram": "mautrix-telegram", - "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.": "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.", - "Enabled Features": "Enabled Features", - "self-hosted webhook bridge": "self-hosted webhook bridge ", - "Self-hosted webhook bridges must have": "Self-hosted webhook bridges must have", - "The public URL for the bridge.": "The public URL for the bridge.", - "The provisioning secret defined in the configuration.": "The provisioning secret defined in the configuration.", - "matrix-appservice-webhooks": "matrix-appservice-webhooks", - "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.": "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.", - "custom bot": "", - "The user ID that Dimension will invite to rooms.": "The user ID that Dimension will invite to rooms.", - "A few words here will help people understand what the bot does.": "A few words here will help people understand what the bot does.", - "Display Name": "Display Name", - "This is the name Dimension will use to tell users which bot this is.": "This is the name Dimension will use to tell users which bot this is.", - "This can either be an MXC URI or a plain URL.": "This can either be an MXC URI or a plain URL.", - "This is used by Dimension to force the bot to leave the room when the user removes the bot.": "This is used by Dimension to force the bot to leave the room when the user removes the bot.", - "Learn more about access tokens.": "Learn more about access tokens.", - "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.": "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.", - "No custom bots.": "No custom bots.", - "Add custom bot": "Add custom bot", - "Custom bots": "Custom bots", - "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.": "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.", - "Administrators": "Administrators", - "Widget Blacklist": "Widget Blacklist", - "Homeserver": "Homeserver", - "Federation URL": "Federation URL", - "Federation Hostname": "Federation Hostname", - "Client/Server URL": "Client/Server URL", - "Utility User ID": "Utility User ID", - "Sessions": "Settings", - "Tokens registered": "Tokens registered", - "Logout Everyone": "Logout Everyone", - "Configuration": "Configuration", - "Logout confirmation": "Logout confirmation", - "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.": "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.", - "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.": "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.", - "User Prefix": "User Prefix", - "This is the prefix used for all bot users.": "This is the prefix used for all bot users.", - "API URL": "API URL", - "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.": "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.", - "New self-hosted go-neb": "New self-hosted go-neb", - "go-neb appservice configuration": "go-neb appservice configuration", - "Copy and paste this configuration to": "", - "on your homeserver and register it as an application service.": "", - "Test Configuration": "Test Configuration", - "Giphy Configuration": "Giphy Configuration", - "Api Key": "Api Key", - "The API key from": "The API key from", - "Image Size": "Image Size", - "GIFs can be large, and sometimes it is more desirable to have them downsized.": "GIFs can be large, and sometimes it is more desirable to have them downsized.", - "Use downsized images": "Use downsized images", - "Google Configuration": "Google Configuration", - "The API key for your Google Application.": "The API key for your Google Application.", - "The search engine ID": "The search engine ID", - "Guggy Configuration": "Guggy Configuration", - "The API key for": "The API key for", - "Imgur Configuration": "Imgur Configuration", - "Client ID": "Client ID", - "The client ID of your": "The client ID of your", - "Imgur Application": "Imgur Application", - "Client Secret": "Client Secret", - "The client secret of your": "The client secret of your", - "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.": "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.", - "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).": "go-neb is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).", - "Enabled Bots": "Enabled Bots", - "No go-neb configurations.": "No go-neb configurations.", - "Add matrix.org's go-neb": "Add matrix.org's go-neb", - "Add self-hosted go-neb": "Add self-hosted go-neb", - "go-neb configurations": "go-neb configurations", - "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.": "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.", - "Import from Telegram": "Import from Telegram", - "Author": "Author", - "License": "License", - "No sticker packs installed.": "No sticker packs installed.", - "Dimension": "Dimension", - "version": "", - "The translated name of your policy": "The translated name of your policy", - "Policy text": "Policy text", - "This is where you put your policy's content.": "This is where you put your policy's content.", - "Create draft": "Create draft", - "Save draft": "Save draft", - "Publish": "Publish", - "Publish policy": "Publish policy", - "Version number": "Version number", - "The version number of this policy": "The version number of this policy", - "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.": "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.", - "Policy Name": "Policy Name", - "Version": "Version", - "No policies written.": "No policies written.", - "New draft policy": "New draft policy", - "Etherpad Widget Configuration": "Etherpad Widget Configuration", - "Default Pad URL Template": "Default Pad URL Template", - "$padName and $roomId will be replaced during creation to help create a unique pad URL.": "$padName and $roomId will be replaced during creation to help create a unique pad URL.", - "Jitsi Widget Configuration": "Jitsi Widget Configuration", - "Jitsi Domain": "Jitsi Domain", - "This is the domain that is used to host the conference.": "This is the domain that is used to host the conference.", - "Use this domain as the default conference domain": "Use this domain as the default conference domain", - "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.": "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.", - "Jitsi Script URL": "Jitsi Script URL", - "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.", - "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.", - "Bridge to Gitter": "Bridge to Gitter", - "This room is bridged to on Gitter": "This room is bridged to", - "on Gitter": "on Gitter", - "Unbridge": "Unbridge", - "Gitter Room": "Gitter Room", - "Add an IRC channel": "Add an IRC channel", - "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.", - "Channel Name": "Channel Name", - "Next": "Next", - "Operator": "Operator", - "The person selected here will be asked to approve or deny the bridge request.": "The person selected here will be asked to approve or deny the bridge request.", - "Request Bridge": "Request Bridge", - "IRC Networks": "IRC Networks", - "Channel": "Channel", - "No bridged channels": "No bridged channels", - "Remove": "Remove", - "Bridge to Slack": "Bridge to Slack", - "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.": "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.", - "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.": "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.", - "Team": "Team", - "Telegram chat is already bridged": "Telegram chat is already bridged", - "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?": "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?", - "Unbridge and continue": "Unbridge and continue", - "No, don't bridge": "No, don't bridge", - "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.": "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.", - "Bridge to Telegram": "Bridge to Telegram", - "This room is bridged to on Telegram": "This room is bridged to", - "on Telegram": "on Telegram", - "You do not have the necessary permissions in this room to unbridge the channel.": "You do not have the necessary permissions in this room to unbridge the channel.", - "After inviting": "After inviting", - "to your Telegram chat, run the command": "to your Telegram chat, run the command", - "in the Telegram room to get the chat ID.": "in the Telegram room to get the chat ID.", - "Add a new webhook": "Add a new webhook", - "Webhook Name": "Webhook Name", - "Create": "Create", - "Type": "Type", - "No webhooks": "No webhooks", - "No name": "No name", - "Delete": "Delete", - "Feeds": "Feeds", - "Added by": "Added by", - "Add": "Add", - ".travis.yml configuration and template information": ".travis.yml configuration and template information", - "The following section needs to be added to your": "", - "file in your repositories:": "", - "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.": "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.", - "The repository identifier": "The repository identifier", - "The repository name": "The repository name", - "The build number": "The build number", - "The build ID": "The build ID", - "The branch name": "The branch name", - "A short version of the commit SHA": "A short version of the commit SHA", - "The first line of the commit message": "The first line of the commit message", - "The full commit message": "The full commit message", - "The author of the commit": "The author of the commit", - "The result of the build": " '%result' - The result of the build", - "The message from Travis CI about the build": "The message from Travis CI about the build", - "The total duration of all builds in the matrix": "The total duration of all builds in the matrix", - "The time it took to run the build": "The time it took to run the build", - "A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build", - "A URL to see the build information": "A URL to see the build information", - "Repositories": "Repositories", - "Repository": "", - "Template": "Template", - "Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.", - "Start a conversation with": "Start a conversation with", - "to create your own stickerpack.": "to create your own stickerpack.", - "Add stickerpack": "Add stickerpack", - "Created by": "Created by", - "under": "under", - "BigBlueButton Meeting URL": "", - "Add Widget": "Add Widget", - "Save Widget": "Save Widget", - "Remove Widget": "Remove Widget", - "Widget Name": "Widget Name", - "Widget URL": "Widget URL", - "Pad Name": "Pad Name", - "Pad URL": "Pad URL", - "Shared Calendar ID": "Shared Calendar ID", - "Document URL": "Document URL", - "Grafana URL": "Grafana URL", - "To get a URL, go to Grafana and click 'share' on a graph.": "To get a URL, go to Grafana and click 'share' on a graph.", - "Conference URL": "Conference URL", - " Spotify URI": "", - "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.": "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.", - "Trading Pair": "Trading Pair", - "Interval": "Interval", - "Whiteboard Name": "Whiteboard Name", - "Whiteboard URL": "Whiteboard URL", - "Video URL": "Video URL", - "An open source integration manager for Matrix": "An open source integration manager for Matrix", - "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", - "source": "source", - "Try it out or": "Try it out or", - "run your own": "run your own", - "Visit": "Visit", - "and log in with your Matrix account or point your Element": "and log in with your Matrix account or point your Element", - "at our servers:": "at our servers:", - "Add utility for everyone in your room by embedding an application.": "Add utility for everyone in your room by embedding an application.", - "Notes": "Notes", - "Google Calendar": "Google Calendar", - "Custom Widget": "Custom Widget", - "Bots bring entertainment or productivity to the room. They're here to help at your command.": "Bots bring entertainment or productivity to the room. They're here to help at your command.", - "Guggy": "Guggy", - "Giphy": "Giphy", - "Imgur": "Imgur", - "Google Image Search": "Google Image Search", - "Wikipedia": "Wikipedia", - "Travis CI": "Travis CI", - "RSS Notifications": "RSS Notifications", - "Echo": "Echo", - "Bring the outside world into your room with bridges.": "Bring the outside world into your room with bridges.", - "Stickers": "Stickers", - "Have some fun and post a sticker.": "Have some fun and post a sticker.", - "Huskies": "Huskies", - "Cats": "Cats", - "Cat Faces": "Cat Faces", - "Loading Artist": "Loading Artist", - "source on GitHub": "source on GitHub", - "Welcome to Dimension!": "Welcome to Dimension!", - "Join": "Join", - "for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on", - "Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element", - "Configuring integrations": "Configuring integrations", - "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 Element. The gear icon": "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 Element. The gear icon", - "in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.", - "Could not connect to integrations server error": "Could not connect to integrations server error", - "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in": "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in", - "on Matrix, here's a few things to check:": "on Matrix, here's a few things to check:", - "Verify the homeserver configuration in Dimension.": "Verify the homeserver configuration in Dimension.", - "The name, client/server URL, and access token all need to be valid and directed at your homeserver.": "The name, client/server URL, and access token all need to be valid and directed at your homeserver.", - "Verify federation is enabled on your homeserver.": "Verify federation is enabled on your homeserver.", - "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.": "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.", - "Verify that federation is working on your homeserver.": "Verify that federation is working on your homeserver.", - "Using tools like the": "Using tools like the", - "federation tester": "federation tester", - ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", - "Looking for your sticker packs?": "Looking for your sticker packs?", - "Click here": "Click here", - "This room is encrypted": "This room is encrypted", - "Integrations are not encrypted!": "Integrations are not encrypted!", - "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", - "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", - "No integrations available": "No integrations available", - "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", - "BigBlueButton Conference": "BigBlueButton Conference", - "Join Conference": "Join Conference", - "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", - "Start camera:": "", - "You": "You", - "Integrations": "Integrations", - "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.": "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.", - "Check connection": "Check connection", - "Click to start OpenID auth": "Click to start OpenID auth", - "User ID:": "User ID:", - "You have blocked this widget from receiving credentials.": "You have blocked this widget from receiving credentials.", - "An error has occurred - see logs for details": "An error has occurred - see logs for details", - "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.": "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.", - "You have no sticker packs.": "You have no sticker packs.", - "Add some stickers": "Add some stickers", - "Failed to load bridges": "Failed to load bridges", - "Error loading bridges": "Error loading bridges", - "matrix.org's Gitter bridge added": "matrix.org's Gitter bridge added", - "Error adding matrix.org's Gitter Bridge": "Error adding matrix.org's Gitter Bridge", - "Error creating matrix.org's Gitter Bridge": "Error creating matrix.org's Gitter Bridge", - "Failed to get an update Gitter bridge list": "Failed to get an update Gitter bridge list", - "Gitter bridge added": "Gitter bridge added", - "Failed to create Gitter bridge": "Failed to create Gitter bridge", - "Gitter bridge updated": "Gitter bridge updated", - "Failed to update Gitter bridge": "Failed to update Gitter bridge", - "IRC Bridge added": "IRC Bridge added", - "Failed to create IRC bridge": "Failed to create IRC bridge", - "Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.", - "matrix.org's IRC bridge added": "matrix.org's IRC bridge added", - "Error adding matrix.org's IRC Bridge": "Error adding matrix.org's IRC Bridge", - "Error creating matrix.org's IRC Bridge": "Error creating matrix.org's IRC Bridge", - "Failed to get an update IRC bridge list": "Failed to get an update IRC bridge list", - "disabled": "disabled", - "Failed to update network": "Failed to update network", - "Slack bridge added": "Slack bridge added", - "Failed to create Slack bridge": "Failed to create Slack bridge", - "Slack bridge updated": "Slack bridge updated", - "Failed to update Slack bridge": "Failed to update Slack bridge", - "matrix.org's Slack bridge added": "matrix.org's Slack bridge added", - "Error adding matrix.org's Slack Bridge": "Error adding matrix.org's Slack Bridge", - "Error creating matrix.org's Slack Bridge": "Error creating matrix.org's Slack Bridge", - "Failed to get an update Slack bridge list": "Failed to get an update Slack bridge list", - "Telegram bridge added": "Telegram bridge added", - "Failed to create Telegram bridge": "Failed to create Telegram bridge", - "Telegram bridge updated": "Telegram bridge updated", - "Failed to update Telegram bridge": "Failed to update Telegram bridge", - "Failed to get an update Telegram bridge list": "Failed to get an update Telegram bridge list", - "Webhook bridge added": "Webhook bridge added", - "Failed to create Webhook bridge": "Failed to create Webhook bridge", - "Webhook bridge updated": "Webhook bridge updated", - "Failed to update Webhook bridge": "Failed to update Webhook bridge", - "Failed to get an update Webhooks bridge list": "Failed to get an update Webhooks bridge list", - "Please enter a name for the bot": "Please enter a name for the bot", - "Please enter an avatar URL for the bot": "Please enter an avatar URL for the bot", - "Please enter a user ID for the bot": "Please enter a user ID for the bot", - "Please enter a description for the bot": "Please enter a description for the bot", - "Please enter an access token for the bot": "Please enter an access token for the bot", - "Bot updated": "Bot updated", - "Error updating bot": "Error updating bot", - "Error loading go-neb configuration": "Error loading go-neb configuration", - "Failed to get an updated bot list": "Failed to get an updated bot list", - "Everyone has been logged out": "Everyone has been logged out", - "Error logging everyone out": "Error logging everyone out", - "New go-neb created": "New go-neb created", - "Error creating appservice": "Error creating appservice", - "Could not load appservice configuration": "Could not load appservice configuration", - "The appservice appears to be correctly set up": "The appservice appears to be correctly set up", - "The appservice is not correctly set up": "The appservice is not correctly set up", - "Error loading configuration": "Error loading configuration", - "Configuration updated": "Configuration updated", - "Error updating integration": "Error updating integration", - "Integration updated": "Integration updated", - "Failed to configure the integration": "Failed to configure the integration", - "Manual troubleshooting may be requred": "Manual troubleshooting may be requred", - "Could not get go-neb configuration": "Could not get go-neb configuration", - "matrix.org's go-neb added": "matrix.org's go-neb added", - "Click the pencil icon to enable the bots.": "Click the pencil icon to enable the bots.", - "Error adding matrix.org's go-neb": "Error adding matrix.org's go-neb", - "Error creating matrix.org go-neb": "Error creating matrix.org go-neb", - "Failed to load sticker packs": "Failed to load sticker packs", - "Sticker pack updated": "Sticker pack updated", - "Error updating sticker pack": "Error updating sticker pack", - "Telegram sticker pack imported": "Telegram sticker pack imported", - "Error importing sticker pack": "Error importing sticker pack", - "Failed to load policy": "Failed to load policy", - "Failed to load policies": "Failed to load policies", - "Policy published": "Policy published", - "Error publishing policy": "Error publishing policy", - "Please enter a name for all policies": "Please enter a name for all policies", - "Please enter text for all policies": "Please enter text for all policies", - "Draft saved": "Draft saved", - "Error saving policy": "Error saving policy", - "Draft created": "Draft created", - "Error creating document": "Error creating document", - "Please enter a version number": "Please enter a version number", - "Widget updated": "Widget updated", - "Error updating widget": "Error updating widget", - "Failed to load widgets": "Failed to load widgets", - "Error opening configuration page": "Error opening configuration page", - "Failed to load configuration": "Failed to load configuration", - "Error updating configuration": "Error updating configuration", - "Error inviting bridge": "Error inviting bridge", - "Bridge requested": "Bridge requested", - "Error requesting bridge": "Error requesting bridge", - "Bridge removed": "Bridge removed", - "Error removing bridge": "Error removing bridge", - "Please enter a channel name": "Please enter a channel name", - "Error loading channel operators": "Error loading channel operators", - "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", - "Please ensure you are an 'Admin' for the room": "Please ensure you are an 'Admin' for the room", - "Link requested!": "Link requested!", - "The operator selected will have to approve the bridge for it to work": "The operator selected will have to approve the bridge for it to work", - "Failed to request a link": "Failed to request a link", - "Link removed": "Link removed", - "Failed to remove link": "Failed to remove link", - "Error getting Slack authorization information": "Error getting Slack authorization information", - "Error getting teams": "Error getting teams", - "Error getting channels for team": "Error getting channels for team", - "Bridge updated": "Bridge updated", - "Webhook created": "Webhook created", - "Error creating webhook": "Error creating webhook", - "Webhook deleted": "Webhook deleted", - "Error deleting webhook": "Error deleting webhook", - "Please enter a feed URL": "Please enter a feed URL", - "Please enter a repository": "Please enter a repository", - "was invited to the room": "was invited to the room", - "was removed from the room": " was removed from the room", - "Could not update integration status": "Could not update integration status", - "Stickerpack added": "Stickerpack added", - "Error adding stickerpack": "Error adding stickerpack", - "Stickers updated": "Stickers updated", - "Error updating stickers": "Error updating stickers", - "Please enter a URL for the widget": "Please enter a URL for the widget", - "Widget added!": "Widget added!", - "Widget updated!": "Widget updated!", - "Widget deleted!": "Widget deleted!", - "Please enter a video URL": "Please enter a video URL", - "Please enter a YouTube, Vimeo, or DailyMotion video URL": "Please enter a YouTube, Vimeo, or DailyMotion video URL", - "Unable to load Dimension - missing room ID or token.": "Unable to load Dimension - missing room ID or token.", - "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!": "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!", - "Unable to communicate with Dimension due to an unknown error.": "Unable to communicate with Dimension due to an unknown error.", - "You do not appear to have permission to modify widgets in this room": "You do not appear to have permission to modify widgets in this room", - "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.", - "This integration is offline or unavailable": "This integration is offline or unavailable", - "Could not communicate with Element": "Could not communicate with Element", - "The room must be": "The room must be", - "to use this integration": "to use this integration", - "You cannot modify widgets in this room": "You cannot modify widgets in this room", - "Error communicating with Element": "Error communicating with Element", - "Expected to not be able to send specific event types": "Expected to not be able to send specific event types", - "Expected to be able to send specific event types": "Expected to be able to send specific event types", - "Requirement": "Requirement ", - "not found": " not found", - "Failed to take screenshot: iframe not supported": "Failed to take screenshot: iframe not supported", - "Failed to take screenshot": "Failed to take screenshot", - "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.": "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.", - "Please accept the prompt to verify your identity.": "Please accept the prompt to verify your identity.", - "You have blocked this widget from verifying your identity.": "You have blocked this widget from verifying your identity.", - "Checking connectivity to integration manager...": "Checking connectivity to integration manager...", - "Checking connectivity to homeserver...": "Checking connectivity to homeserver...", - "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.": "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.", - "You're all set! Click the button below to re-run the test.": "You're all set! Click the button below to re-run the test.", - "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.": "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.", - "Checking client version...": "Checking client version...", - "Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry", - "Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity", - "Error loading policy": "Error loading policy", - "The location of": "The location of ", - "differs depending on whether the": " differs depending on whether the", - "or": " or ", - "version of Element is used.": " version of Element is used." -} + "Dashboard": "Dashboard", + "Widgets": "Widgets", + "go-neb": "go-neb", + "Custom Bots": "Custom Bots", + "Bridges": "Bridges", + "Sticker Packs": "Sticker Packs", + "Terms of Service": "Terms of Service", + "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.": "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.", + "Name": "Name", + "Description": "Description", + "Actions": "Actions", + "No bridges.": "No bridges.", + "matrix-hookshot": "matrix-hookshot", + "is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.", + "No bridge configurations.": "No bridge configurations.", + "Add self-hosted bridge": "Add self-hosted bridge", + "self-hosted Github bridge": "self-hosted Github bridge", + "Self-hosted Github bridges must have": "Self-hosted Github bridges must have", + "provisioning": "provisioning", + "enabled in the configuration.": "enabled in the configuration.", + "Provisioning URL": "Provisioning URL", + "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.": "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.", + "Shared Secret": "Shared Secret", + "The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.", + "Save": "Save", + "Cancel": "Cancel", + "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.", + "Self-hosted Jira bridges must have": "Self-hosted Jira bridges must have", + "is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.": "is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.", + "Self-hosted Webhook bridges must have": "Self-hosted Webhook bridges must have", + "Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge ", + "Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have", + "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.", + "matrix-appservice-irc": "matrix-appservice-irc", + "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.", + "Enabled Networks": "Enabled Networks", + "This bridge is offline or unavailable.": "This bridge is offline or unavailable.", + "Add matrix.org's bridge": "Add matrix.org's bridge", + "Network": "Network", + "Enabled": "Enabled", + "Close": "Close", + "self-hosted Slack bridge": "self-hosted Slack bridge ", + "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", + "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.", + "matrix-appservice-slack": "matrix-appservice-slack", + "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.", + "self-hosted Telegram bridge": "self-hosted Telegram bridge ", + "Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have", + "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.", + "Promote Telegram Puppeting": "Promote Telegram Puppeting", + "If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.", + "Promote Matrix Puppeting": "Promote Matrix Puppeting", + "If enabled, Dimension will recommend that users log in to their Matrix accounts.": "If enabled, Dimension will recommend that users log in to their Matrix accounts.", + "mautrix-telegram": "mautrix-telegram", + "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.": "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.", + "Enabled Features": "Enabled Features", + "self-hosted webhook bridge": "self-hosted webhook bridge ", + "Self-hosted webhook bridges must have": "Self-hosted webhook bridges must have", + "The public URL for the bridge.": "The public URL for the bridge.", + "The provisioning secret defined in the configuration.": "The provisioning secret defined in the configuration.", + "matrix-appservice-webhooks": "matrix-appservice-webhooks", + "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.": "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.", + "custom bot": "custom bot", + "The user ID that Dimension will invite to rooms.": "The user ID that Dimension will invite to rooms.", + "A few words here will help people understand what the bot does.": "A few words here will help people understand what the bot does.", + "Display Name": "Display Name", + "This is the name Dimension will use to tell users which bot this is.": "This is the name Dimension will use to tell users which bot this is.", + "This can either be an MXC URI or a plain URL.": "This can either be an MXC URI or a plain URL.", + "This is used by Dimension to force the bot to leave the room when the user removes the bot.": "This is used by Dimension to force the bot to leave the room when the user removes the bot.", + "Learn more about access tokens.": "Learn more about access tokens.", + "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.": "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.", + "No custom bots.": "No custom bots.", + "Add custom bot": "Add custom bot", + "Custom bots": "Custom bots", + "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.": "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.", + "Administrators": "Administrators", + "Widget Blacklist": "Widget Blacklist", + "Homeserver": "Homeserver", + "Federation URL": "Federation URL", + "Federation Hostname": "Federation Hostname", + "Client/Server URL": "Client/Server URL", + "Utility User ID": "Utility User ID", + "Sessions": "Settings", + "Tokens registered": "Tokens registered", + "Logout Everyone": "Logout Everyone", + "Configuration": "Configuration", + "Logout confirmation": "Logout confirmation", + "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.": "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.", + "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.": "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.", + "User Prefix": "User Prefix", + "This is the prefix used for all bot users.": "This is the prefix used for all bot users.", + "API URL": "API URL", + "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.": "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.", + "New self-hosted go-neb": "New self-hosted go-neb", + "go-neb appservice configuration": "go-neb appservice configuration", + "Copy and paste this configuration to": "Copy and paste this configuration to", + "on your homeserver and register it as an application service.": "on your homeserver and register it as an application service.", + "Test Configuration": "Test Configuration", + "Giphy Configuration": "Giphy Configuration", + "Api Key": "Api Key", + "The API key from": "The API key from", + "Image Size": "Image Size", + "GIFs can be large, and sometimes it is more desirable to have them downsized.": "GIFs can be large, and sometimes it is more desirable to have them downsized.", + "Use downsized images": "Use downsized images", + "Google Configuration": "Google Configuration", + "The API key for your Google Application.": "The API key for your Google Application.", + "Search Engine ID": "Search Engine ID", + "The search engine ID": "The search engine ID", + "Guggy Configuration": "Guggy Configuration", + "The API key for": "The API key for", + "Imgur Configuration": "Imgur Configuration", + "Client ID": "Client ID", + "The client ID of your": "The client ID of your", + "Imgur Application": "Imgur Application", + "Client Secret": "Client Secret", + "The client secret of your": "The client secret of your", + "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.": "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.", + "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).": "go-neb is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).", + "Enabled Bots": "Enabled Bots", + "No go-neb configurations.": "No go-neb configurations.", + "Add matrix.org's go-neb": "Add matrix.org's go-neb", + "Add self-hosted go-neb": "Add self-hosted go-neb", + "go-neb configurations": "go-neb configurations", + "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.": "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.", + "Import from Telegram": "Import from Telegram", + "Author": "Author", + "License": "License", + "No sticker packs installed.": "No sticker packs installed.", + "Dimension": "Dimension", + "version": "version", + "The translated name of your policy": "The translated name of your policy", + "Policy text": "Policy text", + "This is where you put your policy's content.": "This is where you put your policy's content.", + "Create draft": "Create draft", + "Save draft": "Save draft", + "Publish": "Publish", + "Publish policy": "Publish policy", + "Version number": "Version number", + "The version number of this policy": "The version number of this policy", + "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.": "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.", + "Policy Name": "Policy Name", + "Version": "Version", + "No policies written.": "No policies written.", + "New draft policy": "New draft policy", + "Etherpad Widget Configuration": "Etherpad Widget Configuration", + "Default Pad URL Template": "Default Pad URL Template", + "$padName and $roomId will be replaced during creation to help create a unique pad URL.": "$padName and $roomId will be replaced during creation to help create a unique pad URL.", + "Jitsi Widget Configuration": "Jitsi Widget Configuration", + "Jitsi Domain": "Jitsi Domain", + "This is the domain that is used to host the conference.": "This is the domain that is used to host the conference.", + "Use this domain as the default conference domain": "Use this domain as the default conference domain", + "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.": "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.", + "Jitsi Script URL": "Jitsi Script URL", + "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.", + "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.", + "This room is bridged to": "This room is bridged to", + "Unbridge": "Unbridge", + "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.", + "Sign in with GitHub": "Sign in with GitHub", + "Almost there! Just need to add the bot to your organizations now.": "Almost there! Just need to add the bot to your organizations now.", + "Add to GitHub": "Add to GitHub", + "Organization": "Organization", + "Repository": "Repository", + "Add to another GitHub organization": "Add to another GitHub organization", + "Add more repositories from ": "Add more repositories from ", + "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.", + "Sign in with Jira": "Sign in with Jira", + "Instance": "Instance", + "Project": "Project", + "New webhook": "New webhook", + "Webhook name": "Webhook name", + "My Webhook": "My Webhook", + "Add webhook": "Add webhook", + "Webhooks": "Webhooks", + "No webhooks configured for this room.": "No webhooks configured for this room.", + "No name": "No name", + "Remove webhook": "Remove webhook", + "Add IRC channel": "Add IRC channel", + "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.", + "Channel name (without leading #)": "Channel name (without leading #)", + "Next": "Next", + "The person selected here will be asked to approve or deny the bridge request.": "The person selected here will be asked to approve or deny the bridge request.", + "Operator": "Operator", + "Request bridge": "Request bridge", + "Bridged channels": "Bridged channels", + "No channels are bridged to this room.": "No channels are bridged to this room.", + "Unbridge channel": "Unbridge channel", + "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.": "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.", + "This room is bridged to ": "This room is bridged to ", + " on Slack.": " on Slack.", + "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.": "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.", + "Team": "Team", + "Channel": "Channel", + "Bridge": "Bridge", + "Telegram chat is already bridged": "Telegram chat is already bridged", + "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?": "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?", + "Unbridge and continue": "Unbridge and continue", + "No, don't bridge": "No, don't bridge", + "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.": "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.", + "This room is bridged to on Telegram": "This room is bridged to", + "on Telegram": "on Telegram", + "You do not have the necessary permissions in this room to unbridge the channel.": "You do not have the necessary permissions in this room to unbridge the channel.", + "After inviting": "After inviting", + "to your Telegram chat, run the command": "to your Telegram chat, run the command", + "in the Telegram room to get the chat ID.": "in the Telegram room to get the chat ID.", + "Chat ID": "Chat ID", + "Feeds": "Feeds", + "Added by": "Added by", + "Remove": "Remove", + "Add": "Add", + ".travis.yml configuration and template information": ".travis.yml configuration and template information", + "The following section needs to be added to your": "The following section needs to be added to your", + "file in your repositories:": "file in your repositories:", + "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.": "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.", + "The repository identifier": "The repository identifier", + "The repository name": "The repository name", + "The build number": "The build number", + "The build ID": "The build ID", + "The branch name": "The branch name", + "A short version of the commit SHA": "A short version of the commit SHA", + "The first line of the commit message": "The first line of the commit message", + "The full commit message": "The full commit message", + "The author of the commit": "The author of the commit", + "The result of the build": " '%result' - The result of the build", + "The message from Travis CI about the build": "The message from Travis CI about the build", + "The total duration of all builds in the matrix": "The total duration of all builds in the matrix", + "The time it took to run the build": "The time it took to run the build", + "A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build", + "A URL to see the build information": "A URL to see the build information", + "Repositories": "Repositories", + "Template": "Template", + "Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.", + "Start a conversation with": "Start a conversation with", + "to create your own stickerpack.": "to create your own stickerpack.", + "Add stickerpack": "Add stickerpack", + "Created by": "Created by", + "under": "under", + "Meeting URL": "Meeting URL", + "New meeting": "New meeting", + "Meetings": "Meetings", + "Add widget": "Add widget", + "Remove widget": "Remove widget", + "Site name": "Site name", + "Site URL": "Site URL", + "Embed new website": "Embed new website", + "Embedded websites": "Embedded websites", + "Pad name": "Pad name", + "Pad URL": "Pad URL", + "Create new notepad": "Create new notepad", + "Notepads": "Notepads", + "Shared calendar ID": "Shared calendar ID", + "New calendar": "New calendar", + "Calendars": "Calendars", + "Document URL": "Document URL", + "New document": "New document", + "Documents": "Documents", + "To get a URL, go to Grafana and click 'share' on a graph.": "To get a URL, go to Grafana and click 'share' on a graph.", + "Dashboard name": "Dashboard name", + "Dashboard URL": "Dashboard URL", + "New dashboard": "New dashboard", + "Dashboards": "Dashboards", + "Conference URL": "Conference URL", + "Start conference": "Start conference", + "Ongoing conferences": "Ongoing conferences", + "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.": "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.", + "Spotify URI": "Spotify URI", + "Embed music": "Embed music", + "Embedded music": "Embedded music", + "Trading pair": "Trading pair", + "Interval": "Interval", + "Channel name": "Channel name", + "Embed livestream": "Embed livestream", + "Livestreams": "Livestreams", + "URL": "URL", + "New whiteboard": "New whiteboard", + "Whiteboards": "Whiteboards", + "Video URL": "Video URL", + "New video": "New video", + "Videos": "Videos", + "This room is encrypted": "This room is encrypted", + "Integrations are not encrypted!": "Integrations are not encrypted!", + "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", + "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", + "No integrations available": "No integrations available", + "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", + "Sticker packs": "Sticker packs", + "Manage your sticker packs": "Manage your sticker packs", + "here": "here", + "An open source integration manager for Matrix": "An open source integration manager for Matrix", + "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", + "source": "source", + "Try it out or": "Try it out or", + "run your own": "run your own", + "Visit": "Visit", + "and log in with your Matrix account or point your Element": "and log in with your Matrix account or point your Element", + "at our servers:": "at our servers:", + "Add utility for everyone in your room by embedding an application.": "Add utility for everyone in your room by embedding an application.", + "Notes": "Notes", + "Google Calendar": "Google Calendar", + "Custom Widget": "Custom Widget", + "Bots bring entertainment or productivity to the room. They're here to help at your command.": "Bots bring entertainment or productivity to the room. They're here to help at your command.", + "Guggy": "Guggy", + "Giphy": "Giphy", + "Imgur": "Imgur", + "Google Image Search": "Google Image Search", + "Wikipedia": "Wikipedia", + "Travis CI": "Travis CI", + "RSS Notifications": "RSS Notifications", + "Echo": "Echo", + "Bring the outside world into your room with bridges.": "Bring the outside world into your room with bridges.", + "Stickers": "Stickers", + "Have some fun and post a sticker.": "Have some fun and post a sticker.", + "Huskies": "Huskies", + "Cats": "Cats", + "Cat Faces": "Cat Faces", + "Loading Artist": "Loading Artist", + "source on GitHub": "source on GitHub", + "Welcome to Dimension!": "Welcome to Dimension!", + "Join": "Join", + "for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on", + "Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element", + "The location of": "The location of ", + "differs depending on whether the": " differs depending on whether the", + "or": " or ", + "version of Element is used.": " version of Element is used.", + "Configuring integrations": "Configuring integrations", + "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 Element. The gear icon": "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 Element. The gear icon", + "in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.", + "Could not connect to integrations server error": "Could not connect to integrations server error", + "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in": "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in", + "on Matrix, here's a few things to check:": "on Matrix, here's a few things to check:", + "Verify the homeserver configuration in Dimension.": "Verify the homeserver configuration in Dimension.", + "The name, client/server URL, and access token all need to be valid and directed at your homeserver.": "The name, client/server URL, and access token all need to be valid and directed at your homeserver.", + "Verify federation is enabled on your homeserver.": "Verify federation is enabled on your homeserver.", + "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.": "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.", + "Verify that federation is working on your homeserver.": "Verify that federation is working on your homeserver.", + "Using tools like the": "Using tools like the", + "federation tester": "federation tester", + ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", + "Browse integrations": "Browse integrations", + "BigBlueButton Conference": "BigBlueButton Conference", + "Join Conference": "Join Conference", + "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", + "Start camera:": "Start camera:", + "You": "You", + "Integrations": "Integrations", + "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.": "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.", + "Check connection": "Check connection", + "Click to start OpenID auth": "Click to start OpenID auth", + "User ID:": "User ID:", + "You have blocked this widget from receiving credentials.": "You have blocked this widget from receiving credentials.", + "An error has occurred - see logs for details": "An error has occurred - see logs for details", + "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.": "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.", + "You have no sticker packs.": "You have no sticker packs.", + "Add some stickers": "Add some stickers", + "Failed to load bridges": "Failed to load bridges", + "Error loading bridges": "Error loading bridges", + "Failed to get an updated Github bridge list": "Failed to get an updated Github bridge list", + "Github bridge added": "Github bridge added", + "Failed to create Github bridge": "Failed to create Github bridge", + "Github bridge updated": "Github bridge updated", + "Failed to update Github bridge": "Failed to update Github bridge", + "Failed to get an updated Jira bridge list": "Failed to get an updated Jira bridge list", + "Jira bridge added": "Jira bridge added", + "Failed to create Jira bridge": "Failed to create Jira bridge", + "Jira bridge updated": "Jira bridge updated", + "Failed to update Jira bridge": "Failed to update Jira bridge", + "Failed to get an updated Webhook bridge list": "Failed to get an updated Webhook bridge list", + "Webhook bridge added": "Webhook bridge added", + "Failed to create Webhook bridge": "Failed to create Webhook bridge", + "Webhook bridge updated": "Webhook bridge updated", + "Failed to update Webhook bridge": "Failed to update Webhook bridge", + "IRC Bridge added": "IRC Bridge added", + "Failed to create IRC bridge": "Failed to create IRC bridge", + "Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.", + "matrix.org's IRC bridge added": "matrix.org's IRC bridge added", + "Error adding matrix.org's IRC Bridge": "Error adding matrix.org's IRC Bridge", + "Error creating matrix.org's IRC Bridge": "Error creating matrix.org's IRC Bridge", + "Failed to get an update IRC bridge list": "Failed to get an update IRC bridge list", + "disabled": "disabled", + "Failed to update network": "Failed to update network", + "Slack bridge added": "Slack bridge added", + "Failed to create Slack bridge": "Failed to create Slack bridge", + "Slack bridge updated": "Slack bridge updated", + "Failed to update Slack bridge": "Failed to update Slack bridge", + "matrix.org's Slack bridge added": "matrix.org's Slack bridge added", + "Error adding matrix.org's Slack Bridge": "Error adding matrix.org's Slack Bridge", + "Error creating matrix.org's Slack Bridge": "Error creating matrix.org's Slack Bridge", + "Failed to get an update Slack bridge list": "Failed to get an update Slack bridge list", + "Telegram bridge added": "Telegram bridge added", + "Failed to create Telegram bridge": "Failed to create Telegram bridge", + "Telegram bridge updated": "Telegram bridge updated", + "Failed to update Telegram bridge": "Failed to update Telegram bridge", + "Failed to get an update Telegram bridge list": "Failed to get an update Telegram bridge list", + "Failed to get an update Webhooks bridge list": "Failed to get an update Webhooks bridge list", + "Please enter a name for the bot": "Please enter a name for the bot", + "Please enter an avatar URL for the bot": "Please enter an avatar URL for the bot", + "Please enter a user ID for the bot": "Please enter a user ID for the bot", + "Please enter a description for the bot": "Please enter a description for the bot", + "Please enter an access token for the bot": "Please enter an access token for the bot", + "Bot updated": "Bot updated", + "Error updating bot": "Error updating bot", + "Error loading go-neb configuration": "Error loading go-neb configuration", + "Failed to get an updated bot list": "Failed to get an updated bot list", + "Everyone has been logged out": "Everyone has been logged out", + "Error logging everyone out": "Error logging everyone out", + "New go-neb created": "New go-neb created", + "Error creating appservice": "Error creating appservice", + "Could not load appservice configuration": "Could not load appservice configuration", + "The appservice appears to be correctly set up": "The appservice appears to be correctly set up", + "The appservice is not correctly set up": "The appservice is not correctly set up", + "Error loading configuration": "Error loading configuration", + "Configuration updated": "Configuration updated", + "Error updating integration": "Error updating integration", + "Integration updated": "Integration updated", + "Failed to configure the integration": "Failed to configure the integration", + "Manual troubleshooting may be requred": "Manual troubleshooting may be requred", + "Could not get go-neb configuration": "Could not get go-neb configuration", + "matrix.org's go-neb added": "matrix.org's go-neb added", + "Click the pencil icon to enable the bots.": "Click the pencil icon to enable the bots.", + "Error adding matrix.org's go-neb": "Error adding matrix.org's go-neb", + "Error creating matrix.org go-neb": "Error creating matrix.org go-neb", + "Failed to load sticker packs": "Failed to load sticker packs", + "Sticker pack updated": "Sticker pack updated", + "Error updating sticker pack": "Error updating sticker pack", + "Telegram sticker pack imported": "Telegram sticker pack imported", + "Error importing sticker pack": "Error importing sticker pack", + "Failed to load policy": "Failed to load policy", + "Failed to load policies": "Failed to load policies", + "Policy published": "Policy published", + "Error publishing policy": "Error publishing policy", + "Please enter a name for all policies": "Please enter a name for all policies", + "Please enter text for all policies": "Please enter text for all policies", + "Draft saved": "Draft saved", + "Error saving policy": "Error saving policy", + "Draft created": "Draft created", + "Error creating document": "Error creating document", + "Please enter a version number": "Please enter a version number", + "Widget updated": "Widget updated", + "Error updating widget": "Error updating widget", + "Failed to load widgets": "Failed to load widgets", + "Error opening configuration page": "Error opening configuration page", + "Failed to load configuration": "Failed to load configuration", + "Error updating configuration": "Error updating configuration", + "Error getting Github information": "Error getting Github information", + "Error inviting bridge": "Error inviting bridge", + "Bridge requested": "Bridge requested", + "Error requesting bridge": "Error requesting bridge", + "Bridge removed": "Bridge removed", + "Error removing bridge": "Error removing bridge", + "Error getting Jira information": "Error getting Jira information", + "Webhook created": "Webhook created", + "Error creating webhook": "Error creating webhook", + "Webhook deleted": "Webhook deleted", + "Error deleting webhook": "Error deleting webhook", + "Please enter a channel name": "Please enter a channel name", + "Error loading channel operators": "Error loading channel operators", + "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", + "Please ensure you are an 'Admin' for the room": "Please ensure you are an 'Admin' for the room", + "Link requested!": "Link requested!", + "The operator selected will have to approve the bridge for it to work": "The operator selected will have to approve the bridge for it to work", + "Failed to request a link": "Failed to request a link", + "Link removed": "Link removed", + "Failed to remove link": "Failed to remove link", + "Error getting Slack authorization information": "Error getting Slack authorization information", + "Error getting teams": "Error getting teams", + "Error getting channels for team": "Error getting channels for team", + "Bridge updated": "Bridge updated", + "Please enter a feed URL": "Please enter a feed URL", + "Please enter a repository": "Please enter a repository", + "Stickerpack added": "Stickerpack added", + "Error adding stickerpack": "Error adding stickerpack", + "Stickers updated": "Stickers updated", + "Error updating stickers": "Error updating stickers", + "Please enter a URL for the widget": "Please enter a URL for the widget", + "Widget added!": "Widget added!", + "Widget updated!": "Widget updated!", + "Widget deleted!": "Widget deleted!", + "Please enter a video URL": "Please enter a video URL", + "Please enter a YouTube, Vimeo, or DailyMotion video URL": "Please enter a YouTube, Vimeo, or DailyMotion video URL", + "Unable to load Dimension - missing room ID or token.": "Unable to load Dimension - missing room ID or token.", + "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!": "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!", + "Unable to communicate with Dimension due to an unknown error.": "Unable to communicate with Dimension due to an unknown error.", + "You do not appear to have permission to modify widgets in this room": "You do not appear to have permission to modify widgets in this room", + "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.", + "This integration is offline or unavailable": "This integration is offline or unavailable", + "Could not communicate with Element": "Could not communicate with Element", + "You cannot modify widgets in this room": "You cannot modify widgets in this room", + "Error communicating with Element": "Error communicating with Element", + "Expected to not be able to send specific event types": "Expected to not be able to send specific event types", + "Expected to be able to send specific event types": "Expected to be able to send specific event types", + "Requirement": "Requirement ", + "not found": " not found", + "Failed to take screenshot: iframe not supported": "Failed to take screenshot: iframe not supported", + "Failed to take screenshot": "Failed to take screenshot", + "was invited to the room": "was invited to the room", + "was removed from the room": " was removed from the room", + "Could not update integration status: ": "Could not update integration status: ", + "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.": "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.", + "Please accept the prompt to verify your identity.": "Please accept the prompt to verify your identity.", + "You have blocked this widget from verifying your identity.": "You have blocked this widget from verifying your identity.", + "Checking connectivity to integration manager...": "Checking connectivity to integration manager...", + "Checking connectivity to homeserver...": "Checking connectivity to homeserver...", + "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.": "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.", + "You're all set! Click the button below to re-run the test.": "You're all set! Click the button below to re-run the test.", + "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.": "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.", + "Checking client version...": "Checking client version...", + "Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry", + "Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity", + "Error loading policy": "Error loading policy" +} \ No newline at end of file diff --git a/web/assets/i18n/template.json b/web/assets/i18n/template.json index 274d8df..7cc2be8 100644 --- a/web/assets/i18n/template.json +++ b/web/assets/i18n/template.json @@ -1,467 +1,512 @@ { - "Dashboard": "Dashboard", - "Widgets": "Widgets", - "go-neb": "go-neb", - "Custom Bots": "Custom Bots", - "Bridges": "Bridges", - "Sticker Packs": "Sticker Packs", - "Terms of Service": "Terms of Service", - "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.": "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.", - "Name": "Name", - "Description": "Description", - "Actions": "Actions", - "No bridges.": "No bridges.", - "matrix-appservice-gitter": "matrix-appservice-gitter", - "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.": "is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.", - "No bridge configurations.": "No bridge configurations.", - "Add matrix.org's bridge": "Add matrix.org's bridge", - "Add self-hosted bridge": "Add self-hosted bridge", - "self-hosted Gitter bridge": "self-hosted Gitter bridge", - "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", - "Provisioning URL": "Provisioning URL", - "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.", - "Save": "Save", - "Cancel": "Cancel", - "Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge", - "Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have", - "provisioning": "provisioning", - "enabled in the configuration.": "enabled in the configuration.", - "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.", - "matrix-appservice-irc": "matrix-appservice-irc", - "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.", - "Enabled Networks": "Enabled Networks", - "This bridge is offline or unavailable.": "This bridge is offline or unavailable.", - "Network": "Network", - "Enabled": "Enabled", - "Close": "Close", - "self-hosted Slack bridge": "self-hosted Slack bridge", - "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", - "matrix-appservice-slack": "matrix-appservice-slack", - "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.", - "self-hosted Telegram bridge": "self-hosted Telegram bridge", - "Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have", - "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.", - "Shared Secret": "Shared Secret", - "The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.", - "Promote Telegram Puppeting": "Promote Telegram Puppeting", - "If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.", - "Promote Matrix Puppeting": "Promote Matrix Puppeting", - "If enabled, Dimension will recommend that users log in to their Matrix accounts.": "If enabled, Dimension will recommend that users log in to their Matrix accounts.", - "mautrix-telegram": "mautrix-telegram", - "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.": "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.", - "Enabled Features": "Enabled Features", - "self-hosted webhook bridge": "self-hosted webhook bridge", - "Self-hosted webhook bridges must have": "Self-hosted webhook bridges must have", - "The public URL for the bridge.": "The public URL for the bridge.", - "The provisioning secret defined in the configuration.": "The provisioning secret defined in the configuration.", - "matrix-appservice-webhooks": "matrix-appservice-webhooks", - "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.": "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.", - "custom bot": "custom bot", - "The user ID that Dimension will invite to rooms.": "The user ID that Dimension will invite to rooms.", - "A few words here will help people understand what the bot does.": "A few words here will help people understand what the bot does.", - "Display Name": "Display Name", - "This is the name Dimension will use to tell users which bot this is.": "This is the name Dimension will use to tell users which bot this is.", - "This can either be an MXC URI or a plain URL.": "This can either be an MXC URI or a plain URL.", - "This is used by Dimension to force the bot to leave the room when the user removes the bot.": "This is used by Dimension to force the bot to leave the room when the user removes the bot.", - "Learn more about access tokens.": "Learn more about access tokens.", - "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.": "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.", - "No custom bots.": "No custom bots.", - "Add custom bot": "Add custom bot", - "Custom bots": "Custom bots", - "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.": "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.", - "Administrators": "Administrators", - "Widget Blacklist": "Widget Blacklist", - "Homeserver": "Homeserver", - "Federation URL": "Federation URL", - "Federation Hostname": "Federation Hostname", - "Client/Server URL": "Client/Server URL", - "Utility User ID": "Utility User ID", - "Sessions": "Sessions", - "Tokens registered": "Tokens registered", - "Logout Everyone": "Logout Everyone", - "Configuration": "Configuration", - "Logout confirmation": "Logout confirmation", - "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.": "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.", - "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.": "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.", - "User Prefix": "User Prefix", - "This is the prefix used for all bot users.": "This is the prefix used for all bot users.", - "API URL": "API URL", - "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.": "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.", - "New self-hosted go-neb": "New self-hosted go-neb", - "go-neb appservice configuration": "go-neb appservice configuration", - "Copy and paste this configuration to": "Copy and paste this configuration to", - "on your homeserver and register it as an application service.": "on your homeserver and register it as an application service.", - "Test Configuration": "Test Configuration", - "Giphy Configuration": "Giphy Configuration", - "Api Key": "Api Key", - "The API key from": "The API key from", - "Image Size": "Image Size", - "GIFs can be large, and sometimes it is more desirable to have them downsized.": "GIFs can be large, and sometimes it is more desirable to have them downsized.", - "Use downsized images": "Use downsized images", - "Google Configuration": "Google Configuration", - "The API key for your Google Application.": "The API key for your Google Application.", - "The search engine ID": "The search engine ID", - "Guggy Configuration": "Guggy Configuration", - "The API key for": "The API key for", - "Imgur Configuration": "Imgur Configuration", - "Client ID": "Client ID", - "The client ID of your": "The client ID of your", - "Imgur Application": "Imgur Application", - "Client Secret": "Client Secret", - "The client secret of your": "The client secret of your", - "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.": "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.", - "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).": "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).", - "Enabled Bots": "Enabled Bots", - "No go-neb configurations.": "No go-neb configurations.", - "Add matrix.org's go-neb": "Add matrix.org's go-neb", - "Add self-hosted go-neb": "Add self-hosted go-neb", - "go-neb configurations": "go-neb configurations", - "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.": "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.", - "Import from Telegram": "Import from Telegram", - "Author": "Author", - "License": "License", - "No sticker packs installed.": "No sticker packs installed.", - "Dimension": "Dimension", - "version": "version", - "The translated name of your policy": "The translated name of your policy", - "Policy text": "Policy text", - "This is where you put your policy's content.": "This is where you put your policy's content.", - "Create draft": "Create draft", - "Save draft": "Save draft", - "Publish": "Publish", - "Publish policy": "Publish policy", - "Version number": "Version number", - "The version number of this policy": "The version number of this policy", - "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.": "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.", - "Policy Name": "Policy Name", - "Version": "Version", - "No policies written.": "No policies written.", - "New draft policy": "New draft policy", - "Etherpad Widget Configuration": "Etherpad Widget Configuration", - "Default Pad URL Template": "Default Pad URL Template", - "$padName and $roomId will be replaced during creation to help create a unique pad URL.": "$padName and $roomId will be replaced during creation to help create a unique pad URL.", - "Jitsi Widget Configuration": "Jitsi Widget Configuration", - "Jitsi Domain": "Jitsi Domain", - "This is the domain that is used to host the conference.": "This is the domain that is used to host the conference.", - "Use this domain as the default conference domain": "Use this domain as the default conference domain", - "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.": "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.", - "Jitsi Script URL": "Jitsi Script URL", - "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.", - "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.", - "Bridge to Gitter": "Bridge to Gitter", - "This room is bridged to on Gitter": "This room is bridged to on Gitter", - "on Gitter": "on Gitter", - "Unbridge": "Unbridge", - "Gitter Room": "Gitter Room", - "Add an IRC channel": "Add an IRC channel", - "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.", - "Channel Name": "Channel Name", - "Next": "Next", - "Operator": "Operator", - "The person selected here will be asked to approve or deny the bridge request.": "The person selected here will be asked to approve or deny the bridge request.", - "Request Bridge": "Request Bridge", - "IRC Networks": "IRC Networks", - "Channel": "Channel", - "No bridged channels": "No bridged channels", - "Remove": "Remove", - "Bridge to Slack": "Bridge to Slack", - "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.": "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.", - "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.": "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.", - "Team": "Team", - "Telegram chat is already bridged": "Telegram chat is already bridged", - "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?": "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?", - "Unbridge and continue": "Unbridge and continue", - "No, don't bridge": "No, don't bridge", - "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.": "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.", - "Bridge to Telegram": "Bridge to Telegram", - "This room is bridged to on Telegram": "This room is bridged to on Telegram", - "on Telegram": "on Telegram", - "You do not have the necessary permissions in this room to unbridge the channel.": "You do not have the necessary permissions in this room to unbridge the channel.", - "After inviting": "After inviting", - "to your Telegram chat, run the command": "to your Telegram chat, run the command", - "in the Telegram room to get the chat ID.": "in the Telegram room to get the chat ID.", - "Add a new webhook": "Add a new webhook", - "Webhook Name": "Webhook Name", - "Create": "Create", - "Type": "Type", - "No webhooks": "No webhooks", - "No name": "No name", - "Delete": "Delete", - "Feeds": "Feeds", - "Added by": "Added by", - "Add": "Add", - ".travis.yml configuration and template information": ".travis.yml configuration and template information", - "The following section needs to be added to your": "The following section needs to be added to your", - "file in your repositories:": "file in your repositories:", - "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.": "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.", - "The repository identifier": "The repository identifier", - "The repository name": "The repository name", - "The build number": "The build number", - "The build ID": "The build ID", - "The branch name": "The branch name", - "A short version of the commit SHA": "A short version of the commit SHA", - "The first line of the commit message": "The first line of the commit message", - "The full commit message": "The full commit message", - "The author of the commit": "The author of the commit", - "The result of the build": "The result of the build", - "The message from Travis CI about the build": "The message from Travis CI about the build", - "The total duration of all builds in the matrix": "The total duration of all builds in the matrix", - "The time it took to run the build": "The time it took to run the build", - "A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build", - "A URL to see the build information": "A URL to see the build information", - "Repositories": "Repositories", - "Repository": "Repository", - "Template": "Template", - "Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.", - "Start a conversation with": "Start a conversation with", - "to create your own stickerpack.": "to create your own stickerpack.", - "Add stickerpack": "Add stickerpack", - "Created by": "Created by", - "under": "under", - "BigBlueButton Meeting URL": "BigBlueButton Meeting URL", - "Add Widget": "Add Widget", - "Save Widget": "Save Widget", - "Remove Widget": "Remove Widget", - "Widget Name": "Widget Name", - "Widget URL": "Widget URL", - "Pad Name": "Pad Name", - "Pad URL": "Pad URL", - "Shared Calendar ID": "Shared Calendar ID", - "Document URL": "Document URL", - "Grafana URL": "Grafana URL", - "To get a URL, go to Grafana and click 'share' on a graph.": "To get a URL, go to Grafana and click 'share' on a graph.", - "Conference URL": "Conference URL", - " Spotify URI": " Spotify URI", - "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.": "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.", - "Trading Pair": "Trading Pair", - "Interval": "Interval", - "Whiteboard Name": "Whiteboard Name", - "Whiteboard URL": "Whiteboard URL", - "Video URL": "Video URL", - "An open source integration manager for Matrix": "An open source integration manager for Matrix", - "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", - "source": "source", - "Try it out or": "Try it out or", - "run your own": "run your own", - "Visit": "Visit", - "and log in with your Matrix account or point your Element": "and log in with your Matrix account or point your Element", - "at our servers:": "at our servers:", - "Add utility for everyone in your room by embedding an application.": "Add utility for everyone in your room by embedding an application.", - "Notes": "Notes", - "Google Calendar": "Google Calendar", - "Custom Widget": "Custom Widget", - "Bots bring entertainment or productivity to the room. They're here to help at your command.": "Bots bring entertainment or productivity to the room. They're here to help at your command.", - "Guggy": "Guggy", - "Giphy": "Giphy", - "Imgur": "Imgur", - "Google Image Search": "Google Image Search", - "Wikipedia": "Wikipedia", - "Travis CI": "Travis CI", - "RSS Notifications": "RSS Notifications", - "Echo": "Echo", - "Bring the outside world into your room with bridges.": "Bring the outside world into your room with bridges.", - "Stickers": "Stickers", - "Have some fun and post a sticker.": "Have some fun and post a sticker.", - "Huskies": "Huskies", - "Cats": "Cats", - "Cat Faces": "Cat Faces", - "Loading Artist": "Loading Artist", - "source on GitHub": "source on GitHub", - "Welcome to Dimension!": "Welcome to Dimension!", - "Join": "Join", - "for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on", - "Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element", - "Configuring integrations": "Configuring integrations", - "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 Element. The gear icon": "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 Element. The gear icon", - "in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.", - "Could not connect to integrations server error": "Could not connect to integrations server error", - "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in": "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in", - "on Matrix, here's a few things to check:": "on Matrix, here's a few things to check:", - "Verify the homeserver configuration in Dimension.": "Verify the homeserver configuration in Dimension.", - "The name, client/server URL, and access token all need to be valid and directed at your homeserver.": "The name, client/server URL, and access token all need to be valid and directed at your homeserver.", - "Verify federation is enabled on your homeserver.": "Verify federation is enabled on your homeserver.", - "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.": "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.", - "Verify that federation is working on your homeserver.": "Verify that federation is working on your homeserver.", - "Using tools like the": "Using tools like the", - "federation tester": "federation tester", - ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", - "Looking for your sticker packs?": "Looking for your sticker packs?", - "Click here": "Click here", - "This room is encrypted": "This room is encrypted", - "Integrations are not encrypted!": "Integrations are not encrypted!", - "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", - "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", - "No integrations available": "No integrations available", - "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", - "BigBlueButton Conference": "BigBlueButton Conference", - "Join Conference": "Join Conference", - "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", - "Start camera:": "Start camera:", - "You": "You", - "Integrations": "Integrations", - "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.": "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.", - "Check connection": "Check connection", - "Click to start OpenID auth": "Click to start OpenID auth", - "User ID:": "User ID:", - "You have blocked this widget from receiving credentials.": "You have blocked this widget from receiving credentials.", - "An error has occurred - see logs for details": "An error has occurred - see logs for details", - "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.": "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.", - "You have no sticker packs.": "You have no sticker packs.", - "Add some stickers": "Add some stickers", - "Failed to load bridges": "Failed to load bridges", - "Error loading bridges": "Error loading bridges", - "matrix.org's Gitter bridge added": "matrix.org's Gitter bridge added", - "Error adding matrix.org's Gitter Bridge": "Error adding matrix.org's Gitter Bridge", - "Error creating matrix.org's Gitter Bridge": "Error creating matrix.org's Gitter Bridge", - "Failed to get an update Gitter bridge list": "Failed to get an update Gitter bridge list", - "Gitter bridge added": "Gitter bridge added", - "Failed to create Gitter bridge": "Failed to create Gitter bridge", - "Gitter bridge updated": "Gitter bridge updated", - "Failed to update Gitter bridge": "Failed to update Gitter bridge", - "IRC Bridge added": "IRC Bridge added", - "Failed to create IRC bridge": "Failed to create IRC bridge", - "Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.", - "matrix.org's IRC bridge added": "matrix.org's IRC bridge added", - "Error adding matrix.org's IRC Bridge": "Error adding matrix.org's IRC Bridge", - "Error creating matrix.org's IRC Bridge": "Error creating matrix.org's IRC Bridge", - "Failed to get an update IRC bridge list": "Failed to get an update IRC bridge list", - "disabled": "disabled", - "Failed to update network": "Failed to update network", - "Slack bridge added": "Slack bridge added", - "Failed to create Slack bridge": "Failed to create Slack bridge", - "Slack bridge updated": "Slack bridge updated", - "Failed to update Slack bridge": "Failed to update Slack bridge", - "matrix.org's Slack bridge added": "matrix.org's Slack bridge added", - "Error adding matrix.org's Slack Bridge": "Error adding matrix.org's Slack Bridge", - "Error creating matrix.org's Slack Bridge": "Error creating matrix.org's Slack Bridge", - "Failed to get an update Slack bridge list": "Failed to get an update Slack bridge list", - "Telegram bridge added": "Telegram bridge added", - "Failed to create Telegram bridge": "Failed to create Telegram bridge", - "Telegram bridge updated": "Telegram bridge updated", - "Failed to update Telegram bridge": "Failed to update Telegram bridge", - "Failed to get an update Telegram bridge list": "Failed to get an update Telegram bridge list", - "Webhook bridge added": "Webhook bridge added", - "Failed to create Webhook bridge": "Failed to create Webhook bridge", - "Webhook bridge updated": "Webhook bridge updated", - "Failed to update Webhook bridge": "Failed to update Webhook bridge", - "Failed to get an update Webhooks bridge list": "Failed to get an update Webhooks bridge list", - "Please enter a name for the bot": "Please enter a name for the bot", - "Please enter an avatar URL for the bot": "Please enter an avatar URL for the bot", - "Please enter a user ID for the bot": "Please enter a user ID for the bot", - "Please enter a description for the bot": "Please enter a description for the bot", - "Please enter an access token for the bot": "Please enter an access token for the bot", - "Bot updated": "Bot updated", - "Error updating bot": "Error updating bot", - "Error loading go-neb configuration": "Error loading go-neb configuration", - "Failed to get an updated bot list": "Failed to get an updated bot list", - "Everyone has been logged out": "Everyone has been logged out", - "Error logging everyone out": "Error logging everyone out", - "New go-neb created": "New go-neb created", - "Error creating appservice": "Error creating appservice", - "Could not load appservice configuration": "Could not load appservice configuration", - "The appservice appears to be correctly set up": "The appservice appears to be correctly set up", - "The appservice is not correctly set up": "The appservice is not correctly set up", - "Error loading configuration": "Error loading configuration", - "Configuration updated": "Configuration updated", - "Error updating integration": "Error updating integration", - "Integration updated": "Integration updated", - "Failed to configure the integration": "Failed to configure the integration", - "Manual troubleshooting may be requred": "Manual troubleshooting may be requred", - "Could not get go-neb configuration": "Could not get go-neb configuration", - "matrix.org's go-neb added": "matrix.org's go-neb added", - "Click the pencil icon to enable the bots.": "Click the pencil icon to enable the bots.", - "Error adding matrix.org's go-neb": "Error adding matrix.org's go-neb", - "Error creating matrix.org go-neb": "Error creating matrix.org go-neb", - "Failed to load sticker packs": "Failed to load sticker packs", - "Sticker pack updated": "Sticker pack updated", - "Error updating sticker pack": "Error updating sticker pack", - "Telegram sticker pack imported": "Telegram sticker pack imported", - "Error importing sticker pack": "Error importing sticker pack", - "Failed to load policy": "Failed to load policy", - "Failed to load policies": "Failed to load policies", - "Policy published": "Policy published", - "Error publishing policy": "Error publishing policy", - "Please enter a name for all policies": "Please enter a name for all policies", - "Please enter text for all policies": "Please enter text for all policies", - "Draft saved": "Draft saved", - "Error saving policy": "Error saving policy", - "Draft created": "Draft created", - "Error creating document": "Error creating document", - "Please enter a version number": "Please enter a version number", - "Widget updated": "Widget updated", - "Error updating widget": "Error updating widget", - "Failed to load widgets": "Failed to load widgets", - "Error opening configuration page": "Error opening configuration page", - "Failed to load configuration": "Failed to load configuration", - "Error updating configuration": "Error updating configuration", - "Error inviting bridge": "Error inviting bridge", - "Bridge requested": "Bridge requested", - "Error requesting bridge": "Error requesting bridge", - "Bridge removed": "Bridge removed", - "Error removing bridge": "Error removing bridge", - "Please enter a channel name": "Please enter a channel name", - "Error loading channel operators": "Error loading channel operators", - "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", - "Please ensure you are an 'Admin' for the room": "Please ensure you are an 'Admin' for the room", - "Link requested!": "Link requested!", - "The operator selected will have to approve the bridge for it to work": "The operator selected will have to approve the bridge for it to work", - "Failed to request a link": "Failed to request a link", - "Link removed": "Link removed", - "Failed to remove link": "Failed to remove link", - "Error getting Slack authorization information": "Error getting Slack authorization information", - "Error getting teams": "Error getting teams", - "Error getting channels for team": "Error getting channels for team", - "Bridge updated": "Bridge updated", - "Webhook created": "Webhook created", - "Error creating webhook": "Error creating webhook", - "Webhook deleted": "Webhook deleted", - "Error deleting webhook": "Error deleting webhook", - "Please enter a feed URL": "Please enter a feed URL", - "Please enter a repository": "Please enter a repository", - "was invited to the room": "was invited to the room", - "was removed from the room": "was removed from the room", - "Could not update integration status": "Could not update integration status", - "Stickerpack added": "Stickerpack added", - "Error adding stickerpack": "Error adding stickerpack", - "Stickers updated": "Stickers updated", - "Error updating stickers": "Error updating stickers", - "Please enter a URL for the widget": "Please enter a URL for the widget", - "Widget added!": "Widget added!", - "Widget updated!": "Widget updated!", - "Widget deleted!": "Widget deleted!", - "Please enter a video URL": "Please enter a video URL", - "Please enter a YouTube, Vimeo, or DailyMotion video URL": "Please enter a YouTube, Vimeo, or DailyMotion video URL", - "Unable to load Dimension - missing room ID or token.": "Unable to load Dimension - missing room ID or token.", - "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!": "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!", - "Unable to communicate with Dimension due to an unknown error.": "Unable to communicate with Dimension due to an unknown error.", - "You do not appear to have permission to modify widgets in this room": "You do not appear to have permission to modify widgets in this room", - "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.", - "This integration is offline or unavailable": "This integration is offline or unavailable", - "Could not communicate with Element": "Could not communicate with Element", - "The room must be": "The room must be", - "to use this integration": "to use this integration", - "You cannot modify widgets in this room": "You cannot modify widgets in this room", - "Error communicating with Element": "Error communicating with Element", - "Expected to not be able to send specific event types": "Expected to not be able to send specific event types", - "Expected to be able to send specific event types": "Expected to be able to send specific event types", - "Requirement": "Requirement", - "not found": "not found", - "Failed to take screenshot: iframe not supported": "Failed to take screenshot: iframe not supported", - "Failed to take screenshot": "Failed to take screenshot", - "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.": "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.", - "Please accept the prompt to verify your identity.": "Please accept the prompt to verify your identity.", - "You have blocked this widget from verifying your identity.": "You have blocked this widget from verifying your identity.", - "Checking connectivity to integration manager...": "Checking connectivity to integration manager...", - "Checking connectivity to homeserver...": "Checking connectivity to homeserver...", - "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.": "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.", - "You're all set! Click the button below to re-run the test.": "You're all set! Click the button below to re-run the test.", - "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.": "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.", - "Checking client version...": "Checking client version...", - "Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry", - "Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity", - "Error loading policy": "Error loading policy", - "The location of": "The location of", - "differs depending on whether the": "differs depending on whether the", - "or": "or", - "version of Element is used.": "version of Element is used." -} + "Dashboard": "Dashboard", + "Widgets": "Widgets", + "go-neb": "go-neb", + "Custom Bots": "Custom Bots", + "Bridges": "Bridges", + "Sticker Packs": "Sticker Packs", + "Terms of Service": "Terms of Service", + "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.": "Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For example, an IRC bridge can allow IRC and matrix users to communicate with each other.", + "Name": "Name", + "Description": "Description", + "Actions": "Actions", + "No bridges.": "No bridges.", + "matrix-hookshot": "matrix-hookshot", + "is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Github as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.", + "No bridge configurations.": "No bridge configurations.", + "Add self-hosted bridge": "Add self-hosted bridge", + "self-hosted Github bridge": "self-hosted Github bridge", + "Self-hosted Github bridges must have": "Self-hosted Github bridges must have", + "provisioning": "provisioning", + "enabled in the configuration.": "enabled in the configuration.", + "Provisioning URL": "Provisioning URL", + "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.": "The provisioning URL for the bridge. This is the specific address for the bridge given in the configuration.", + "Shared Secret": "Shared Secret", + "The shared secret defined in the configuration for provisioning.": "The shared secret defined in the configuration for provisioning.", + "Save": "Save", + "Cancel": "Cancel", + "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.": "is a multi-purpose bridge which supports Jira as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a room to pipe to a repository.", + "Self-hosted Jira bridges must have": "Self-hosted Jira bridges must have", + "is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.": "is a multi-purpose bridge which supports Generic Webhooks as an integration. If enabled in the configuration, it can be used here to offer a UI for setting up a webhook into the room.", + "Self-hosted Webhook bridges must have": "Self-hosted Webhook bridges must have", + "Add a new self-hosted IRC Bridge": "Add a new self-hosted IRC Bridge", + "Self-hosted IRC bridges must have": "Self-hosted IRC bridges must have", + "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.": "The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.", + "matrix-appservice-irc": "matrix-appservice-irc", + "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.": "is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC bridges to better distribute the load across multiple networks in large deployments.", + "Enabled Networks": "Enabled Networks", + "This bridge is offline or unavailable.": "This bridge is offline or unavailable.", + "Add matrix.org's bridge": "Add matrix.org's bridge", + "Network": "Network", + "Enabled": "Enabled", + "Close": "Close", + "self-hosted Slack bridge": "self-hosted Slack bridge", + "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.": "Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.", + "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.": "The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.", + "matrix-appservice-slack": "matrix-appservice-slack", + "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.": "is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their Slack workspaces and from there they can pick the channels they'd like to bridge.", + "self-hosted Telegram bridge": "self-hosted Telegram bridge", + "Self-hosted Telegram bridges must have": "Self-hosted Telegram bridges must have", + "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.": "The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.", + "Promote Telegram Puppeting": "Promote Telegram Puppeting", + "If enabled, Dimension will recommend that users log in to their Telegram accounts.": "If enabled, Dimension will recommend that users log in to their Telegram accounts.", + "Promote Matrix Puppeting": "Promote Matrix Puppeting", + "If enabled, Dimension will recommend that users log in to their Matrix accounts.": "If enabled, Dimension will recommend that users log in to their Matrix accounts.", + "mautrix-telegram": "mautrix-telegram", + "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.": "is a Telegram bridge that supports bridging channels/supergroups to Matrix. This can be done through a 'relay bot' (all Matrix users are represented through a single bot in Telegram) or by making use of a user's real Telegram account (known as 'puppeting'). Currently only one Telegram bridge can be configured at a time.", + "Enabled Features": "Enabled Features", + "self-hosted webhook bridge": "self-hosted webhook bridge", + "Self-hosted webhook bridges must have": "Self-hosted webhook bridges must have", + "The public URL for the bridge.": "The public URL for the bridge.", + "The provisioning secret defined in the configuration.": "The provisioning secret defined in the configuration.", + "matrix-appservice-webhooks": "matrix-appservice-webhooks", + "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.": "provides Slack-compatible webhooks for Matrix, making it easy to send updates into a room.", + "custom bot": "custom bot", + "The user ID that Dimension will invite to rooms.": "The user ID that Dimension will invite to rooms.", + "A few words here will help people understand what the bot does.": "A few words here will help people understand what the bot does.", + "Display Name": "Display Name", + "This is the name Dimension will use to tell users which bot this is.": "This is the name Dimension will use to tell users which bot this is.", + "This can either be an MXC URI or a plain URL.": "This can either be an MXC URI or a plain URL.", + "This is used by Dimension to force the bot to leave the room when the user removes the bot.": "This is used by Dimension to force the bot to leave the room when the user removes the bot.", + "Learn more about access tokens.": "Learn more about access tokens.", + "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.": "Custom bots give you the ability to add your own bots to Dimension for users to add to their rooms. These bots can't have any configuration to them and must be able to accept room invites on their own. All Dimension will do when a user wants to add the bot is invite it to the room.", + "No custom bots.": "No custom bots.", + "Add custom bot": "Add custom bot", + "Custom bots": "Custom bots", + "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.": "Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.", + "Administrators": "Administrators", + "Widget Blacklist": "Widget Blacklist", + "Homeserver": "Homeserver", + "Federation URL": "Federation URL", + "Federation Hostname": "Federation Hostname", + "Client/Server URL": "Client/Server URL", + "Utility User ID": "Utility User ID", + "Sessions": "Sessions", + "Tokens registered": "Tokens registered", + "Logout Everyone": "Logout Everyone", + "Configuration": "Configuration", + "Logout confirmation": "Logout confirmation", + "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.": "Logging everyone out will disable all known login tokens for Dimension and upstream integration managers. Most clients will automatically re-register for a login token behind the scenes, similar to how a login token was first acquired.", + "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.": "Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.", + "User Prefix": "User Prefix", + "This is the prefix used for all bot users.": "This is the prefix used for all bot users.", + "API URL": "API URL", + "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.": "The admin/api url for go-neb. Be sure to not expose the admin API to the outside world because this endpoint is not authenticated.", + "New self-hosted go-neb": "New self-hosted go-neb", + "go-neb appservice configuration": "go-neb appservice configuration", + "Copy and paste this configuration to": "Copy and paste this configuration to", + "on your homeserver and register it as an application service.": "on your homeserver and register it as an application service.", + "Test Configuration": "Test Configuration", + "Giphy Configuration": "Giphy Configuration", + "Api Key": "Api Key", + "The API key from": "The API key from", + "Image Size": "Image Size", + "GIFs can be large, and sometimes it is more desirable to have them downsized.": "GIFs can be large, and sometimes it is more desirable to have them downsized.", + "Use downsized images": "Use downsized images", + "Google Configuration": "Google Configuration", + "The API key for your Google Application.": "The API key for your Google Application.", + "Search Engine ID": "Search Engine ID", + "The search engine ID": "The search engine ID", + "Guggy Configuration": "Guggy Configuration", + "The API key for": "The API key for", + "Imgur Configuration": "Imgur Configuration", + "Client ID": "Client ID", + "The client ID of your": "The client ID of your", + "Imgur Application": "Imgur Application", + "Client Secret": "Client Secret", + "The client secret of your": "The client secret of your", + "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.": "go-neb supports many different types of bots, each of which is listed below. Here you can configure which bots this go-neb instance should use.", + "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).": "is a multi-purpose bot that can provide various services, such as reaction GIFs and Github notifications. There are two options for go-neb support in Dimension: using matrix.org's or self-hosting it. Each go-neb instance can have multiple services associated with it (ie: one go-neb here can do everything).", + "Enabled Bots": "Enabled Bots", + "No go-neb configurations.": "No go-neb configurations.", + "Add matrix.org's go-neb": "Add matrix.org's go-neb", + "Add self-hosted go-neb": "Add self-hosted go-neb", + "go-neb configurations": "go-neb configurations", + "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.": "Sticker packs provide a way to convey memes or feelings to others in a room. From here you're able to select which sticker packs users of this Dimension instance can use. If no sticker packs are enabled then the 'sticker picker' widget will be disabled.", + "Import from Telegram": "Import from Telegram", + "Author": "Author", + "License": "License", + "No sticker packs installed.": "No sticker packs installed.", + "Dimension": "Dimension", + "version": "version", + "The translated name of your policy": "The translated name of your policy", + "Policy text": "Policy text", + "This is where you put your policy's content.": "This is where you put your policy's content.", + "Create draft": "Create draft", + "Save draft": "Save draft", + "Publish": "Publish", + "Publish policy": "Publish policy", + "Version number": "Version number", + "The version number of this policy": "The version number of this policy", + "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.": "Before users can use Dimension they must agree to the terms of service for using your instance. If you're using any matrix.org bridges, users will be required to accept the terms of service for your upstream integration managers (scalar.vector.im usually) in addition to the terms you add here.", + "Policy Name": "Policy Name", + "Version": "Version", + "No policies written.": "No policies written.", + "New draft policy": "New draft policy", + "Etherpad Widget Configuration": "Etherpad Widget Configuration", + "Default Pad URL Template": "Default Pad URL Template", + "$padName and $roomId will be replaced during creation to help create a unique pad URL.": "$padName and $roomId will be replaced during creation to help create a unique pad URL.", + "Jitsi Widget Configuration": "Jitsi Widget Configuration", + "Jitsi Domain": "Jitsi Domain", + "This is the domain that is used to host the conference.": "This is the domain that is used to host the conference.", + "Use this domain as the default conference domain": "Use this domain as the default conference domain", + "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.": "Some clients can create widgets that are not compatible with Dimension, making Dimension use jitsi.riot.im by default. By enabling this option, you'll force Dimension to use your Jitsi domain at risk of having clients not respect it.", + "Jitsi Script URL": "Jitsi Script URL", + "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.": "This is used to create the Jitsi widget. It is normally at /libs/external_api.min.js from your domain.", + "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.": "Widgets are small webpages that can be embedded in a Matrix room. Here you can configure which widgets Dimension will offer to users.", + "This room is bridged to": "This room is bridged to", + "Unbridge": "Unbridge", + "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Github, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.", + "Sign in with GitHub": "Sign in with GitHub", + "Almost there! Just need to add the bot to your organizations now.": "Almost there! Just need to add the bot to your organizations now.", + "Add to GitHub": "Add to GitHub", + "Organization": "Organization", + "Repository": "Repository", + "Add to another GitHub organization": "Add to another GitHub organization", + "Add more repositories from ": "Add more repositories from ", + "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.": "In order to bridge to Jira, you'll need to authorize the bridge to access your organization(s). Please click the button below to do so.", + "Sign in with Jira": "Sign in with Jira", + "Instance": "Instance", + "Project": "Project", + "New webhook": "New webhook", + "Webhook name": "Webhook name", + "My Webhook": "My Webhook", + "Add webhook": "Add webhook", + "Webhooks": "Webhooks", + "No webhooks configured for this room.": "No webhooks configured for this room.", + "No name": "No name", + "Remove webhook": "Remove webhook", + "Add IRC channel": "Add IRC channel", + "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.": "Bridging a channel requires authorization from a channel operator. When entering a channel below, a bot will join the channel to ensure it exists and has operators available.", + "Channel name (without leading #)": "Channel name (without leading #)", + "Next": "Next", + "The person selected here will be asked to approve or deny the bridge request.": "The person selected here will be asked to approve or deny the bridge request.", + "Operator": "Operator", + "Request bridge": "Request bridge", + "Bridged channels": "Bridged channels", + "No channels are bridged to this room.": "No channels are bridged to this room.", + "Unbridge channel": "Unbridge channel", + "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.": "This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as rich bridging as the new approach. It is recommended to re-create the bridge with the new process.", + "This room is bridged to ": "This room is bridged to ", + " on Slack.": " on Slack.", + "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.": "In order to bridge Slack channels, you'll need to authorize the bridge to access your teams and channels. Please click the button below to do so.", + "Team": "Team", + "Channel": "Channel", + "Bridge": "Bridge", + "Telegram chat is already bridged": "Telegram chat is already bridged", + "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?": "You have the appropriate permissions to be able to unbridge the chat, however. Would you like to unbridge the other room and instead bridge it here?", + "Unbridge and continue": "Unbridge and continue", + "No, don't bridge": "No, don't bridge", + "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.": "That Telegram chat is bridged to another Matrix room and cannot be bridged here. Unfortunately, you do not have the required permissions to be able to unbridge the other room.", + "This room is bridged to on Telegram": "This room is bridged to on Telegram", + "on Telegram": "on Telegram", + "You do not have the necessary permissions in this room to unbridge the channel.": "You do not have the necessary permissions in this room to unbridge the channel.", + "After inviting": "After inviting", + "to your Telegram chat, run the command": "to your Telegram chat, run the command", + "in the Telegram room to get the chat ID.": "in the Telegram room to get the chat ID.", + "Chat ID": "Chat ID", + "Feeds": "Feeds", + "Added by": "Added by", + "Remove": "Remove", + "Add": "Add", + ".travis.yml configuration and template information": ".travis.yml configuration and template information", + "The following section needs to be added to your": "The following section needs to be added to your", + "file in your repositories:": "file in your repositories:", + "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.": "The following variables can be used in your template. This template is used to post a message to theroom when your webhook is activated.", + "The repository identifier": "The repository identifier", + "The repository name": "The repository name", + "The build number": "The build number", + "The build ID": "The build ID", + "The branch name": "The branch name", + "A short version of the commit SHA": "A short version of the commit SHA", + "The first line of the commit message": "The first line of the commit message", + "The full commit message": "The full commit message", + "The author of the commit": "The author of the commit", + "The result of the build": "The result of the build", + "The message from Travis CI about the build": "The message from Travis CI about the build", + "The total duration of all builds in the matrix": "The total duration of all builds in the matrix", + "The time it took to run the build": "The time it took to run the build", + "A URL to see the changes which triggered the build": "A URL to see the changes which triggered the build", + "A URL to see the build information": "A URL to see the build information", + "Repositories": "Repositories", + "Template": "Template", + "Sticker packs are not enabled on this Dimension instance.": "Sticker packs are not enabled on this Dimension instance.", + "Start a conversation with": "Start a conversation with", + "to create your own stickerpack.": "to create your own stickerpack.", + "Add stickerpack": "Add stickerpack", + "Created by": "Created by", + "under": "under", + "Meeting URL": "Meeting URL", + "New meeting": "New meeting", + "Meetings": "Meetings", + "Add widget": "Add widget", + "Remove widget": "Remove widget", + "Site name": "Site name", + "Site URL": "Site URL", + "Embed new website": "Embed new website", + "Embedded websites": "Embedded websites", + "Pad name": "Pad name", + "Pad URL": "Pad URL", + "Create new notepad": "Create new notepad", + "Notepads": "Notepads", + "Shared calendar ID": "Shared calendar ID", + "New calendar": "New calendar", + "Calendars": "Calendars", + "Document URL": "Document URL", + "New document": "New document", + "Documents": "Documents", + "To get a URL, go to Grafana and click 'share' on a graph.": "To get a URL, go to Grafana and click 'share' on a graph.", + "Dashboard name": "Dashboard name", + "Dashboard URL": "Dashboard URL", + "New dashboard": "New dashboard", + "Dashboards": "Dashboards", + "Conference URL": "Conference URL", + "Start conference": "Start conference", + "Ongoing conferences": "Ongoing conferences", + "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.": "Click 'share' from your favourite playlist, artist, track, or album and paste the Spotify URI here.", + "Spotify URI": "Spotify URI", + "Embed music": "Embed music", + "Embedded music": "Embedded music", + "Trading pair": "Trading pair", + "Interval": "Interval", + "Channel name": "Channel name", + "Embed livestream": "Embed livestream", + "Livestreams": "Livestreams", + "URL": "URL", + "New whiteboard": "New whiteboard", + "Whiteboards": "Whiteboards", + "Video URL": "Video URL", + "New video": "New video", + "Videos": "Videos", + "This room is encrypted": "This room is encrypted", + "Integrations are not encrypted!": "Integrations are not encrypted!", + "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.": "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 name, your username, your avatar, information about Element, and other similar details. Add integrations with caution.", + "There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!", + "No integrations available": "No integrations available", + "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.", + "Sticker packs": "Sticker packs", + "Manage your sticker packs": "Manage your sticker packs", + "here": "here", + "An open source integration manager for Matrix": "An open source integration manager for Matrix", + "Self-host your favourite bots, bridges, and widgets.": "Self-host your favourite bots, bridges, and widgets.", + "source": "source", + "Try it out or": "Try it out or", + "run your own": "run your own", + "Visit": "Visit", + "and log in with your Matrix account or point your Element": "and log in with your Matrix account or point your Element", + "at our servers:": "at our servers:", + "Add utility for everyone in your room by embedding an application.": "Add utility for everyone in your room by embedding an application.", + "Notes": "Notes", + "Google Calendar": "Google Calendar", + "Custom Widget": "Custom Widget", + "Bots bring entertainment or productivity to the room. They're here to help at your command.": "Bots bring entertainment or productivity to the room. They're here to help at your command.", + "Guggy": "Guggy", + "Giphy": "Giphy", + "Imgur": "Imgur", + "Google Image Search": "Google Image Search", + "Wikipedia": "Wikipedia", + "Travis CI": "Travis CI", + "RSS Notifications": "RSS Notifications", + "Echo": "Echo", + "Bring the outside world into your room with bridges.": "Bring the outside world into your room with bridges.", + "Stickers": "Stickers", + "Have some fun and post a sticker.": "Have some fun and post a sticker.", + "Huskies": "Huskies", + "Cats": "Cats", + "Cat Faces": "Cat Faces", + "Loading Artist": "Loading Artist", + "source on GitHub": "source on GitHub", + "Welcome to Dimension!": "Welcome to Dimension!", + "Join": "Join", + "for news and updates. Don't forget to star the repository on": "for news and updates. Don't forget to star the repository on", + "Here's the configuration options you'll need to update in your Element": "Here's the configuration options you'll need to update in your Element", + "The location of": "The location of", + "differs depending on whether the": "differs depending on whether the", + "or": "or", + "version of Element is used.": "version of Element is used.", + "Configuring integrations": "Configuring integrations", + "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 Element. The gear icon": "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 Element. The gear icon", + "in the top right is where you can configure your bots, bridges, and widgets.": "in the top right is where you can configure your bots, bridges, and widgets.", + "Could not connect to integrations server error": "Could not connect to integrations server error", + "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in": "When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying 'Could not contact integrations server' shows up in every room. Before visiting us in", + "on Matrix, here's a few things to check:": "on Matrix, here's a few things to check:", + "Verify the homeserver configuration in Dimension.": "Verify the homeserver configuration in Dimension.", + "The name, client/server URL, and access token all need to be valid and directed at your homeserver.": "The name, client/server URL, and access token all need to be valid and directed at your homeserver.", + "Verify federation is enabled on your homeserver.": "Verify federation is enabled on your homeserver.", + "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.": "Even in a private, or non-federated, environment federation needs to be enabled so Dimension can work correctly. Dimension should still work okay if federation on your homeserver is bound to a private interface instead of being public - just be sure to set the federation URL in your configuration.", + "Verify that federation is working on your homeserver.": "Verify that federation is working on your homeserver.", + "Using tools like the": "Using tools like the", + "federation tester": "federation tester", + ", make sure that federation is working on your homeserver.": ", make sure that federation is working on your homeserver.", + "Browse integrations": "Browse integrations", + "BigBlueButton Conference": "BigBlueButton Conference", + "Join Conference": "Join Conference", + "Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded", + "Start camera:": "Start camera:", + "You": "You", + "Integrations": "Integrations", + "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.": "Your client is too old to use this widget. Try upgrading your client to the latest available version, or contact the author to try and diagnose the problem. Your client needs to support OpenID information exchange.", + "Check connection": "Check connection", + "Click to start OpenID auth": "Click to start OpenID auth", + "User ID:": "User ID:", + "You have blocked this widget from receiving credentials.": "You have blocked this widget from receiving credentials.", + "An error has occurred - see logs for details": "An error has occurred - see logs for details", + "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.": "There was a problem authenticating your use of this sticker picker. Please make sure you're using a client that has Dimension enabled and correctly set up.", + "You have no sticker packs.": "You have no sticker packs.", + "Add some stickers": "Add some stickers", + "Failed to load bridges": "Failed to load bridges", + "Error loading bridges": "Error loading bridges", + "Failed to get an updated Github bridge list": "Failed to get an updated Github bridge list", + "Github bridge added": "Github bridge added", + "Failed to create Github bridge": "Failed to create Github bridge", + "Github bridge updated": "Github bridge updated", + "Failed to update Github bridge": "Failed to update Github bridge", + "Failed to get an updated Jira bridge list": "Failed to get an updated Jira bridge list", + "Jira bridge added": "Jira bridge added", + "Failed to create Jira bridge": "Failed to create Jira bridge", + "Jira bridge updated": "Jira bridge updated", + "Failed to update Jira bridge": "Failed to update Jira bridge", + "Failed to get an updated Webhook bridge list": "Failed to get an updated Webhook bridge list", + "Webhook bridge added": "Webhook bridge added", + "Failed to create Webhook bridge": "Failed to create Webhook bridge", + "Webhook bridge updated": "Webhook bridge updated", + "Failed to update Webhook bridge": "Failed to update Webhook bridge", + "IRC Bridge added": "IRC Bridge added", + "Failed to create IRC bridge": "Failed to create IRC bridge", + "Click the pencil icon to enable networks.": "Click the pencil icon to enable networks.", + "matrix.org's IRC bridge added": "matrix.org's IRC bridge added", + "Error adding matrix.org's IRC Bridge": "Error adding matrix.org's IRC Bridge", + "Error creating matrix.org's IRC Bridge": "Error creating matrix.org's IRC Bridge", + "Failed to get an update IRC bridge list": "Failed to get an update IRC bridge list", + "disabled": "disabled", + "Failed to update network": "Failed to update network", + "Slack bridge added": "Slack bridge added", + "Failed to create Slack bridge": "Failed to create Slack bridge", + "Slack bridge updated": "Slack bridge updated", + "Failed to update Slack bridge": "Failed to update Slack bridge", + "matrix.org's Slack bridge added": "matrix.org's Slack bridge added", + "Error adding matrix.org's Slack Bridge": "Error adding matrix.org's Slack Bridge", + "Error creating matrix.org's Slack Bridge": "Error creating matrix.org's Slack Bridge", + "Failed to get an update Slack bridge list": "Failed to get an update Slack bridge list", + "Telegram bridge added": "Telegram bridge added", + "Failed to create Telegram bridge": "Failed to create Telegram bridge", + "Telegram bridge updated": "Telegram bridge updated", + "Failed to update Telegram bridge": "Failed to update Telegram bridge", + "Failed to get an update Telegram bridge list": "Failed to get an update Telegram bridge list", + "Failed to get an update Webhooks bridge list": "Failed to get an update Webhooks bridge list", + "Please enter a name for the bot": "Please enter a name for the bot", + "Please enter an avatar URL for the bot": "Please enter an avatar URL for the bot", + "Please enter a user ID for the bot": "Please enter a user ID for the bot", + "Please enter a description for the bot": "Please enter a description for the bot", + "Please enter an access token for the bot": "Please enter an access token for the bot", + "Bot updated": "Bot updated", + "Error updating bot": "Error updating bot", + "Error loading go-neb configuration": "Error loading go-neb configuration", + "Failed to get an updated bot list": "Failed to get an updated bot list", + "Everyone has been logged out": "Everyone has been logged out", + "Error logging everyone out": "Error logging everyone out", + "New go-neb created": "New go-neb created", + "Error creating appservice": "Error creating appservice", + "Could not load appservice configuration": "Could not load appservice configuration", + "The appservice appears to be correctly set up": "The appservice appears to be correctly set up", + "The appservice is not correctly set up": "The appservice is not correctly set up", + "Error loading configuration": "Error loading configuration", + "Configuration updated": "Configuration updated", + "Error updating integration": "Error updating integration", + "Integration updated": "Integration updated", + "Failed to configure the integration": "Failed to configure the integration", + "Manual troubleshooting may be requred": "Manual troubleshooting may be requred", + "Could not get go-neb configuration": "Could not get go-neb configuration", + "matrix.org's go-neb added": "matrix.org's go-neb added", + "Click the pencil icon to enable the bots.": "Click the pencil icon to enable the bots.", + "Error adding matrix.org's go-neb": "Error adding matrix.org's go-neb", + "Error creating matrix.org go-neb": "Error creating matrix.org go-neb", + "Failed to load sticker packs": "Failed to load sticker packs", + "Sticker pack updated": "Sticker pack updated", + "Error updating sticker pack": "Error updating sticker pack", + "Telegram sticker pack imported": "Telegram sticker pack imported", + "Error importing sticker pack": "Error importing sticker pack", + "Failed to load policy": "Failed to load policy", + "Failed to load policies": "Failed to load policies", + "Policy published": "Policy published", + "Error publishing policy": "Error publishing policy", + "Please enter a name for all policies": "Please enter a name for all policies", + "Please enter text for all policies": "Please enter text for all policies", + "Draft saved": "Draft saved", + "Error saving policy": "Error saving policy", + "Draft created": "Draft created", + "Error creating document": "Error creating document", + "Please enter a version number": "Please enter a version number", + "Widget updated": "Widget updated", + "Error updating widget": "Error updating widget", + "Failed to load widgets": "Failed to load widgets", + "Error opening configuration page": "Error opening configuration page", + "Failed to load configuration": "Failed to load configuration", + "Error updating configuration": "Error updating configuration", + "Error getting Github information": "Error getting Github information", + "Error inviting bridge": "Error inviting bridge", + "Bridge requested": "Bridge requested", + "Error requesting bridge": "Error requesting bridge", + "Bridge removed": "Bridge removed", + "Error removing bridge": "Error removing bridge", + "Error getting Jira information": "Error getting Jira information", + "Webhook created": "Webhook created", + "Error creating webhook": "Error creating webhook", + "Webhook deleted": "Webhook deleted", + "Error deleting webhook": "Error deleting webhook", + "Please enter a channel name": "Please enter a channel name", + "Error loading channel operators": "Error loading channel operators", + "Failed to make the bridge an administrator": "Failed to make the bridge an administrator", + "Please ensure you are an 'Admin' for the room": "Please ensure you are an 'Admin' for the room", + "Link requested!": "Link requested!", + "The operator selected will have to approve the bridge for it to work": "The operator selected will have to approve the bridge for it to work", + "Failed to request a link": "Failed to request a link", + "Link removed": "Link removed", + "Failed to remove link": "Failed to remove link", + "Error getting Slack authorization information": "Error getting Slack authorization information", + "Error getting teams": "Error getting teams", + "Error getting channels for team": "Error getting channels for team", + "Bridge updated": "Bridge updated", + "Please enter a feed URL": "Please enter a feed URL", + "Please enter a repository": "Please enter a repository", + "Stickerpack added": "Stickerpack added", + "Error adding stickerpack": "Error adding stickerpack", + "Stickers updated": "Stickers updated", + "Error updating stickers": "Error updating stickers", + "Please enter a URL for the widget": "Please enter a URL for the widget", + "Widget added!": "Widget added!", + "Widget updated!": "Widget updated!", + "Widget deleted!": "Widget deleted!", + "Please enter a video URL": "Please enter a video URL", + "Please enter a YouTube, Vimeo, or DailyMotion video URL": "Please enter a YouTube, Vimeo, or DailyMotion video URL", + "Unable to load Dimension - missing room ID or token.": "Unable to load Dimension - missing room ID or token.", + "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!": "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!", + "Unable to communicate with Dimension due to an unknown error.": "Unable to communicate with Dimension due to an unknown error.", + "You do not appear to have permission to modify widgets in this room": "You do not appear to have permission to modify widgets in this room", + "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.": "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.", + "This integration is offline or unavailable": "This integration is offline or unavailable", + "Could not communicate with Element": "Could not communicate with Element", + "You cannot modify widgets in this room": "You cannot modify widgets in this room", + "Error communicating with Element": "Error communicating with Element", + "Expected to not be able to send specific event types": "Expected to not be able to send specific event types", + "Expected to be able to send specific event types": "Expected to be able to send specific event types", + "Requirement": "Requirement", + "not found": "not found", + "Failed to take screenshot: iframe not supported": "Failed to take screenshot: iframe not supported", + "Failed to take screenshot": "Failed to take screenshot", + "was invited to the room": "was invited to the room", + "was removed from the room": "was removed from the room", + "Could not update integration status: ": "Could not update integration status: ", + "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.": "Click the button to test your connection. This may cause your client to ask if it is okay to share your identity with the widget - this is required to test your connection to your homeserver.", + "Please accept the prompt to verify your identity.": "Please accept the prompt to verify your identity.", + "You have blocked this widget from verifying your identity.": "You have blocked this widget from verifying your identity.", + "Checking connectivity to integration manager...": "Checking connectivity to integration manager...", + "Checking connectivity to homeserver...": "Checking connectivity to homeserver...", + "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.": "Error checking if the integration manager is alive. This usually means that the manager which served this widget has gone offline.", + "You're all set! Click the button below to re-run the test.": "You're all set! Click the button below to re-run the test.", + "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.": "Error contacting homeserver. This usually means your federation setup is incorrect, or your homeserver is offline. Consult your homeserver's documentation for how to set up federation.", + "Checking client version...": "Checking client version...", + "Your client is too old to use this widget, sorry": "Your client is too old to use this widget, sorry", + "Please accept the prompt to verify your identity": "Please accept the prompt to verify your identity", + "Error loading policy": "Error loading policy" +} \ No newline at end of file diff --git a/web/assets/img/avatars/jira.png b/web/assets/img/avatars/jira.png new file mode 100644 index 0000000..0bffc9c Binary files /dev/null and b/web/assets/img/avatars/jira.png differ diff --git a/web/assets/img/avatars/jitsi.png b/web/assets/img/avatars/jitsi.png index 575c4c9..de401fb 100644 Binary files a/web/assets/img/avatars/jitsi.png and b/web/assets/img/avatars/jitsi.png differ diff --git a/web/assets/img/element/chevron-left.svg b/web/assets/img/element/chevron-left.svg new file mode 100644 index 0000000..f78a50a --- /dev/null +++ b/web/assets/img/element/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/assets/img/element/close.svg b/web/assets/img/element/close.svg new file mode 100644 index 0000000..598caed --- /dev/null +++ b/web/assets/img/element/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/assets/img/element/cog.svg b/web/assets/img/element/cog.svg new file mode 100644 index 0000000..b7d83f0 --- /dev/null +++ b/web/assets/img/element/cog.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/index.html b/web/index.html index ddfcc16..05f5435 100644 --- a/web/index.html +++ b/web/index.html @@ -24,11 +24,11 @@ } body.theme-dark { - background-color: #4a4a4a; + background-color: #21262C; } body.theme-light { - background-color: #e7e7e7; + background-color: #ffffff; } .app-loading { diff --git a/web/polyfills.ts b/web/polyfills.ts index ced2732..71b4aa0 100644 --- a/web/polyfills.ts +++ b/web/polyfills.ts @@ -57,9 +57,7 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. - - +import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ diff --git a/web/style/_riot.scss b/web/style/_riot.scss index c90eb73..de14a98 100644 --- a/web/style/_riot.scss +++ b/web/style/_riot.scss @@ -20,24 +20,20 @@ .header .quickAction .closeButton .scalarClose { position: absolute; - top: 19px; - right: 15px; + top: 0px; + right: 0px; } .header .quickAction .adminButton { position: absolute; - top: 19px; - right: 55px; + top: 0px; + right: 26px; } .header .quickAction .headerButton { - font-size: 35px; + width: 20px; + height: 20px; cursor: pointer; - color: themed(quickActionColor); - } - - .header .quickAction .headerButton:hover { - color: themed(quickActionHoverColor); } } } diff --git a/web/style/app.scss b/web/style/app.scss index ba68875..710d10c 100644 --- a/web/style/app.scss +++ b/web/style/app.scss @@ -1,18 +1,19 @@ // styles in src/style directory are applied to the whole page -@import "../assets//fonts/opensans100-roboto300"; +@import "../assets/fonts/inter.scss"; @import "~bootstrap/scss/bootstrap"; @import "~ngx-ui-switch/ui-switch.component.scss"; @import "themes/themes"; +@import "components/icons"; @import "components/ibox"; @import "components/dialog"; @import "components/bootstrap_override"; +@import "components/element"; @import "~angular2-toaster/toaster"; @import "riot"; body { margin: 0; padding: 0; - font-family: "Roboto", "Open Sans", Arial, sans-serif; } @include themifyRoot() { @@ -51,3 +52,8 @@ button { .error-text { color: #bd362f !important; } + +h3 { + font-size: 1.13em; + font-weight: 600; +} diff --git a/web/style/components/bootstrap_override.scss b/web/style/components/bootstrap_override.scss index d8fa372..4932269 100644 --- a/web/style/components/bootstrap_override.scss +++ b/web/style/components/bootstrap_override.scss @@ -4,6 +4,7 @@ .main-app { a { color: themed(anchorColor); + text-decoration: none; } table, td, th { @@ -32,4 +33,4 @@ display: flex; } } -} \ No newline at end of file +} diff --git a/web/style/components/element.scss b/web/style/components/element.scss new file mode 100644 index 0000000..534b500 --- /dev/null +++ b/web/style/components/element.scss @@ -0,0 +1,37 @@ +@import "../themes/themes"; + +.main-app { + @include themify() { + .element-btn { + outline: none; + border: none; + background: themed(buttonAccentColor); + color: themed(buttonPrimaryColor); + text-align: center; + padding: 4px 18px; + border-radius: 8px; + font-size: 0.9em; + + &.element-btn-danger-link { + background: themed(buttonPrimaryColor); + color: themed(buttonDangerColor); + } + + &.element-btn-danger { + background: themed(buttonDangerColor); + } + + &:disabled { + opacity: 0.3; + } + } + + .element-btn-row { + margin-top: 25px; + + button { + margin-right: 8px; + } + } + } +} diff --git a/web/style/components/icons.scss b/web/style/components/icons.scss new file mode 100644 index 0000000..d26eab3 --- /dev/null +++ b/web/style/components/icons.scss @@ -0,0 +1,34 @@ +@import "../themes/themes"; + +@mixin dim-icon { + display: inline-block; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; +} + +@include themifyRoot() { + .dim-icon { + &.dim-chevron-left { + @include dim-icon(); + width: 10px; + height: 10px; + mask-image: url('/assets/img/element/chevron-left.svg'); + background-color: themed(defaultFgColor); + } + &.dim-cog { + @include dim-icon(); + width: 20px; + height: 20px; + mask-image: url('/assets/img/element/cog.svg'); + background-color: themed(mutedColor); + } + &.dim-close { + @include dim-icon(); + width: 20px; + height: 20px; + mask-image: url('/assets/img/element/close.svg'); + background-color: themed(mutedColor); + } + } +} diff --git a/web/style/themes/dark.scss b/web/style/themes/dark.scss index d360e43..bba7f6a 100644 --- a/web/style/themes/dark.scss +++ b/web/style/themes/dark.scss @@ -1,18 +1,24 @@ $theme_dark: ( - bodyBgColor: rgba(47, 50, 51, 0.85), - defaultFgColor: #eaeaea, + bodyBgColor: #21262C, + defaultFgColor: #ffffff, headerColor: #f9f9f9, mutedColor: #d6d6d6, altMutedColor: #a6b6c1, - anchorColor: #82c5ff, + anchorColor: #0DBD8B, tableBorderColor: #1e1e1e, codeBgColor: #323233, spinnerColor: #d4d4d4, - formControlBgColor: #494b4e, + formControlBorderColor: #21262C, + formControlBgColor: #15191E, formControlFgColor: #fff, - formControlPlaceholderColor: #969696, + formControlPlaceholderColor: #8E99A4, + accentAlt: #238CF5, activeBreadcrumbColor: #afbfca, + buttonAccentColor: #0DBD8B, + buttonDangerColor: #FF5B55, + buttonPrimaryColor: #fff, + stickerpacksNotifBgColor: #525252, stickerpackBgColor: #676a6d, stickerpackFgColor: #eee, @@ -21,17 +27,13 @@ $theme_dark: ( stickerPreviewBgColor: #676a6d, stickerPreviewFgColor: #dedddd, - headerBgColor: rgb(47, 47, 47), - headerBorderColor: #585858, - headerGradientStartColor: rgba(201, 80, 54, 0.7), - headerGradientEndColor: rgba(143, 45, 86, 0.7), + headerBgColor: #21262C, containerBgColor: rgba(47, 47, 47, 0.66), containerShadowColor: hsla(0, 0%, 0%, 0.2), containerBorderColor: rgb(79, 79, 79), quickActionColor: #7b7b7b, - quickActionHoverColor: #fff, dialogBgColor: #464646, dialogFgColor: #ececec, @@ -60,7 +62,6 @@ $theme_dark: ( widgetBannedSymbolColor: #bd362f, integrationBorderColor: rgba(148, 148, 148, 0.19), - integrationShadowColor: rgba(89, 98, 105, 0.28), integrationDescriptionColor: #bcc0c1, promoSpacerColor: rgba(206, 222, 235, 0.85), diff --git a/web/style/themes/light.scss b/web/style/themes/light.scss index c1e6a0b..0f6fea8 100644 --- a/web/style/themes/light.scss +++ b/web/style/themes/light.scss @@ -1,18 +1,24 @@ $theme_light: ( - bodyBgColor: rgba(176, 187, 191, 0.12), - defaultFgColor: #333, - headerColor: #333, - mutedColor: #999, - altMutedColor: #636c72, - anchorColor: #0275d8, + bodyBgColor: #ffffff, + defaultFgColor: #17191C, + headerColor: #17191C, + mutedColor: #C1C6CD, + altMutedColor: #C1C6CD, + anchorColor: #0DBD8B, tableBorderColor: #eceeef, codeBgColor: #f7f7f9, - spinnerColor: #333, + spinnerColor: #17191C, + formControlBorderColor: #e7e7e7, formControlBgColor: #fff, - formControlFgColor: #464a4c, - formControlPlaceholderColor: #7d8487, + formControlFgColor: #17191C, + formControlPlaceholderColor: #888, + accentAlt: #238CF5, activeBreadcrumbColor: #636c72, + buttonAccentColor: #0DBD8B, + buttonDangerColor: #FF5B55, + buttonPrimaryColor: #fff, + stickerpacksNotifFgColor: #525252, stickerpackBgColor: #f6fbff, stickerpackFgColor: #222, @@ -22,16 +28,12 @@ $theme_light: ( stickerPreviewFgColor: #7d7d7d, headerBgColor: #fff, - headerBorderColor: #e7eaec, - headerGradientStartColor: rgba(201, 80, 54, 0.7), - headerGradientEndColor: rgba(143, 45, 86, 0.7), containerBgColor: #fff, containerShadowColor: hsla(0, 0%, 0%, 0.2), containerBorderColor: #e7eaec, quickActionColor: #313131, - quickActionHoverColor: #0b0b0b, dialogBgColor: #fff, dialogFgColor: #222, @@ -59,9 +61,8 @@ $theme_light: ( genericControlFgColor: #222, widgetBannedSymbolColor: #bd362f, - integrationBorderColor: rgba(143, 45, 86, 0.2), - integrationShadowColor: rgba(206, 222, 235, 0.34), - integrationDescriptionColor: #999, + integrationBorderColor: #E3E8F0, + integrationDescriptionColor: #737D8C, promoSpacerColor: rgba(206, 222, 235, 0.85), promoShadowColor: rgba(206, 222, 235, 0.34),