Skip to content
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
22ad0e9
Improve domain splitting for "Hosted by" header
luelista Mar 16, 2025
50a25dd
Add preference for custom web instances, use it for Element
luelista Mar 16, 2025
06237b1
Add link to change custom web instance
luelista Mar 16, 2025
d993157
Add form to configure custom web instance
luelista Mar 16, 2025
6dd9a02
Trim whitespace and protocol / path information
luelista Mar 16, 2025
642a74b
Add files via upload
Domoel Mar 27, 2025
b47dbf1
Add files via upload
Domoel Mar 27, 2025
67c27fd
Update compose.yaml
Domoel Mar 27, 2025
4baaa31
Update compose.yaml
Domoel Mar 27, 2025
b7a3507
Update README.md
Domoel Mar 27, 2025
e7a809f
Update README.md
Domoel Mar 27, 2025
8abc0ce
Create docker-image.yml
Domoel Mar 28, 2025
b560e2d
Update Dockerfile
Domoel Mar 28, 2025
3a0359c
Update compose.yaml
Domoel Mar 28, 2025
66cc550
Update Dockerfile
Domoel Mar 28, 2025
3fa1ad7
Update README.md
Domoel Mar 28, 2025
f04e948
Update Dockerfile
Domoel Mar 28, 2025
df2c426
Update compose.yaml
Domoel Mar 28, 2025
1898808
Update README.md
Domoel Mar 28, 2025
70b5a49
Update Dockerfile
Domoel Mar 28, 2025
684f3f7
Update Dockerfile
Domoel Mar 28, 2025
334393b
Update Dockerfile
Domoel Mar 28, 2025
ed1230e
Update Dockerfile
Domoel Mar 28, 2025
46a14f0
Update Dockerfile
Domoel Mar 28, 2025
ef85f67
Update Dockerfile
Domoel Mar 28, 2025
b8047a7
Update Dockerfile
Domoel Mar 28, 2025
4e4e034
Update Dockerfile
Domoel Mar 28, 2025
bda105c
Update compose.yaml
Domoel Mar 28, 2025
8c2db8f
Update README.md
Domoel Mar 28, 2025
8fee384
Update compose.yaml
Domoel Mar 28, 2025
b75ca64
Update README.md
Domoel Mar 28, 2025
84db9ec
Update Dockerfile
Domoel Mar 28, 2025
521b2ad
Update Dockerfile
Domoel Mar 28, 2025
58890a7
Update Dockerfile
Domoel Mar 28, 2025
8124698
Update README.md
Domoel Mar 28, 2025
27ff8f1
Update Dockerfile
Domoel Mar 28, 2025
73200e8
Update compose.yaml
Domoel Mar 28, 2025
05ebb90
Add files via upload
Domoel Mar 28, 2025
4259af7
Update README.md
Domoel Mar 28, 2025
1205705
Update compose.yaml
Domoel Mar 28, 2025
f1d33f3
Update README.md
Domoel Mar 28, 2025
fe21222
Merge pull request #1 from luelista/custom-web-instances
Domoel Mar 28, 2025
b49724d
Update Element.js
Domoel Mar 28, 2025
2e4ce08
Update Element.js
Domoel Mar 28, 2025
28d77a8
Merge pull request #2 from matrix-org/main
Domoel Apr 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Build and Push Matrix-to Docker Image

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
push: true
tags: domoel/matrix-to:latest
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Stage 1: Build
FROM node:20.2-alpine AS build
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build

# Stage 2: Production
FROM nginx:alpine
WORKDIR /etc/nginx
COPY ./nginx.conf /etc/nginx/nginx.conf
WORKDIR /usr/share/nginx/html
COPY --from=build /app/build .

# Expose port 80
EXPOSE 80

# Healthcheck
HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1

# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,29 @@ https://matrix.to/#/#matrix:matrix.org?web-instance[element.io]=chat.mozilla.org
You can discuss matrix.to in
[`#matrix.to:matrix.org`](https://matrix.to/#/#matrix.to:matrix.org)


## Build Instructions

### Native build

1. Install [yarn](https://classic.yarnpkg.com/en/docs/install)
1. `git clone https://github.com/matrix-org/matrix.to`
1. `cd matrix.to`
1. `yarn`
1. `yarn start`
1. Go to http://localhost:5000 in your browser

### Build with docker compose

```
version: '3.8'

services:
matrix-to:
container_name: Matrix-to
image: domoel/matrix-to:latest
ports:
- "1336:80"
environment:
- NODE_ENV=production
```
10 changes: 10 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '3.8'

services:
matrix-to:
container_name: Matrix-to
image: domoel/matrix-to:latest
ports:
- "1336:80"
environment:
- NODE_ENV=production
34 changes: 34 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
worker_processes 1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

sendfile on;
keepalive_timeout 65;

server {
listen 80;
server_name localhost;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
19 changes: 18 additions & 1 deletion src/Preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class Preferences extends EventEmitter {
// used to differentiate web from native if a client supports both
this.platform = null;
this.homeservers = null;
this.customWebInstances = {};

const prefsStr = localStorage.getItem("preferred_client");
if (prefsStr) {
Expand All @@ -36,6 +37,10 @@ export class Preferences extends EventEmitter {
if (serversStr) {
this.homeservers = JSON.parse(serversStr);
}
const customWebInstancesStr = localStorage.getItem("custom_web_instances");
if (customWebInstancesStr) {
this.customWebInstances = JSON.parse(customWebInstancesStr);
}
}

setClient(id, platform) {
Expand All @@ -54,15 +59,27 @@ export class Preferences extends EventEmitter {
}
}

setCustomWebInstance(client_id, instance_url) {
this.customWebInstances[client_id] = instance_url;
this._localStorage.setItem("custom_web_instances", JSON.stringify(this.customWebInstances));
this.emit("canClear");
}

getCustomWebInstance(client_id) {
return this.customWebInstances[client_id];
}

clear() {
this._localStorage.removeItem("preferred_client");
this._localStorage.removeItem("consented_servers");
this._localStorage.removeItem("custom_web_instances");
this.clientId = null;
this.platform = null;
this.homeservers = null;
this.customWebInstances = {};
}

get canClear() {
return !!this.clientId || !!this.platform || !!this.homeservers;
return !!this.clientId || !!this.platform || !!this.homeservers || !!this.customWebInstances;
}
}
47 changes: 47 additions & 0 deletions src/open/ClientView.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ function renderInstructions(parts) {
export class ClientView extends TemplateView {

render(t, vm) {
return t.mapView(vm => vm.customWebInstanceFormOpen, open => {
switch (open) {
case true: return new SetCustomWebInstanceView(vm);
case false: return new TemplateView(vm, t => this.renderContent(t, vm));
}
});
}
renderContent(t, vm) {
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
t.div({className: "header"}, [
Expand Down Expand Up @@ -112,10 +120,49 @@ class InstallClientView extends TemplateView {
}
}

export class SetCustomWebInstanceView extends TemplateView {
render(t, vm) {
return t.div({className: "SetCustomWebInstanceView"}, [
t.p([
"Use a custom web instance for the ", t.strong(vm.name), " client:",
]),
t.form({action: "#", id: "setCustomWebInstanceForm", onSubmit: evt => this._onSubmit(evt)}, [
t.input({
type: "text",
className: "fullwidth large",
placeholder: "chat.example.org",
name: "instanceHostname",
value: vm.preferredWebInstance || "",
}),
t.input({type: "submit", value: "Save", className: "primary fullwidth"}),
t.input({type: "button", value: "Use Default Instance", className: "secondary fullwidth", onClick: evt => this._onReset(evt)}),
])
]);
}

_onSubmit(evt) {
evt.preventDefault();
const form = evt.target;
const {instanceHostname} = form.elements;
this.value.setCustomWebInstance(instanceHostname.value);
this.value.closeCustomWebInstanceForm();
}

_onReset(evt) {
this.value.setCustomWebInstance(undefined);
this.value.closeCustomWebInstanceForm();
}
}

function showBack(t, vm) {
return t.p({className: {caption: true, "back": true, hidden: vm => !vm.showBack}}, [
`Continue with ${vm.name} · `,
t.button({className: "text", onClick: () => vm.back()}, "Change"),
t.span({hidden: vm => !vm.supportsCustomWebInstances}, [
' · ',
t.button({className: "text", onClick: () => vm.configureCustomWebInstance()}, "Use Custom Web Instance"),
])

]);
}

Expand Down
59 changes: 46 additions & 13 deletions src/open/ClientViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class ClientViewModel extends ViewModel {
this._pickClient = pickClient;
// to provide "choose other client" button after calling pick()
this._clientListViewModel = null;
this.customWebInstanceFormOpen = false;
this._update();
}

Expand All @@ -59,11 +60,11 @@ export class ClientViewModel extends ViewModel {
if (this._proposedPlatform === this._nativePlatform) {
deepLinkLabel = "Open in app";
} else {
deepLinkLabel = `Open on ${this._client.getPreferredWebInstance(this._link)}`;
deepLinkLabel = `Open on ${this.preferredWebInstance}`;
}
}
const actions = [];
const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link);
const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link, this.preferredWebInstance);
if (proposedDeepLink) {
actions.push({
label: deepLinkLabel,
Expand All @@ -83,8 +84,8 @@ export class ClientViewModel extends ViewModel {
// show only if there is a preferred instance, and if we don't already link to it in the first button
if (hasPreferredWebInstance && this._webPlatform && this._proposedPlatform !== this._webPlatform) {
actions.push({
label: `Open on ${this._client.getPreferredWebInstance(this._link)}`,
url: this._client.getDeepLink(this._webPlatform, this._link),
label: `Open on ${this.preferredWebInstance}`,
url: this._client.getDeepLink(this._webPlatform, this._link, this.preferredWebInstance),
kind: "open-in-web",
activated: () => {} // don't persist this choice as we don't persist the preferred web instance, it's in the url
});
Expand All @@ -108,10 +109,10 @@ export class ClientViewModel extends ViewModel {
actions.push(...nativeActions);
}
if (this._webPlatform) {
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link);
const webDeepLink = this._client.getDeepLink(this._webPlatform, this._link, this.preferredWebInstance);
if (webDeepLink) {
const webLabel = this.hasPreferredWebInstance ?
`Open on ${this._client.getPreferredWebInstance(this._link)}` :
`Open on ${this.preferredWebInstance}` :
`Continue in your browser`;
actions.push({
label: webLabel,
Expand All @@ -128,18 +129,26 @@ export class ClientViewModel extends ViewModel {
return actions;
}

get hasPreferredWebInstance() {
get preferredWebInstance() {
// also check there is a web platform that matches the platforms the user is on (mobile or desktop web)
return this._webPlatform && typeof this._client.getPreferredWebInstance(this._link) === "string";
if (!this._webPlatform) return undefined;
return (
this.preferences.getCustomWebInstance(this._client.id)
|| this._client.getPreferredWebInstance(this._link)
);
}

get hasPreferredWebInstance() {
return typeof this.preferredWebInstance === "string";
}

get hostedByBannerLabel() {
const preferredWebInstance = this._client.getPreferredWebInstance(this._link);
if (this._webPlatform && preferredWebInstance) {
if (this.hasPreferredWebInstance) {
const preferredWebInstance = this.preferredWebInstance;
let label = preferredWebInstance;
const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf("."));
const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf(".") - 1);
if (subDomainIdx !== -1) {
label = preferredWebInstance.slice(preferredWebInstance.length - subDomainIdx + 1);
label = preferredWebInstance.slice(subDomainIdx + 1);
}
return `Hosted by ${label}`;
}
Expand Down Expand Up @@ -188,7 +197,7 @@ export class ClientViewModel extends ViewModel {

get showDeepLinkInInstall() {
// we can assume this._nativePlatform as this._clientCanIntercept already checks it
return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link);
return this._clientCanIntercept && !!this._client.getDeepLink(this._nativePlatform, this._link, this.preferredWebInstance);
}

get availableOnPlatformNames() {
Expand Down Expand Up @@ -223,6 +232,10 @@ export class ClientViewModel extends ViewModel {
return !!this._clientListViewModel;
}

get supportsCustomWebInstances() {
return !!this._client.supportsCustomInstances;
}

back() {
if (this._clientListViewModel) {
const vm = this._clientListViewModel;
Expand All @@ -231,9 +244,29 @@ export class ClientViewModel extends ViewModel {
// in the list with all clients, and also if we refresh, we get the list with
// all clients rather than having our "change client" click reverted.
this.preferences.setClient(undefined, undefined);
this.preferences.setCustomWebInstance(this._client.id, undefined);
this._update();
this.emitChange();
vm.showAll();
}
}

configureCustomWebInstance() {
this.customWebInstanceFormOpen = true;
this.emitChange();
}

closeCustomWebInstanceForm() {
this.customWebInstanceFormOpen = false;
this.emitChange();
}

setCustomWebInstance(hostname) {
if (hostname) {
hostname = hostname.trim().replace(/^https:\/\//, '').replace(/\/.*$/, '');
}
this.preferences.setClient(this._client.id, hostname ? this._webPlatform : (this._nativePlatform || this._webPlatform));
this.preferences.setCustomWebInstance(this._client.id, hostname || undefined);
this._update();
}
}
7 changes: 4 additions & 3 deletions src/open/clients/Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export class Element {
get homepage() { return "https://element.io"; }
get author() { return "Element"; }
getMaturity(platform) { return Maturity.Stable; }
get supportsCustomInstances() { return true; }

getDeepLink(platform, link) {
getDeepLink(platform, link, preferredWebInstance) {
let fragmentPath;
switch (link.kind) {
case LinkKind.User:
Expand All @@ -83,8 +84,8 @@ export class Element {
let instanceHost = trustedWebInstances[0];
// we use app.element.io which iOS will intercept, but it likely won't intercept any other trusted instances
// so only use a preferred web instance for true web links.
if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) {
instanceHost = link.webInstances[this.id];
if (isWebPlatform && preferredWebInstance) {
instanceHost = preferredWebInstance;
}
return `https://${instanceHost}/#/${fragmentPath}`;
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
Expand Down