82 Commits

Author SHA1 Message Date
Dome 50d03aad1d Update client.css 2026-04-07 21:40:18 +02:00
Dome 29b754c9f8 Update main.css 2026-04-07 21:38:55 +02:00
Dome 9c90ede9c4 Update main.css 2026-04-07 21:34:53 +02:00
Dome 35bdc7473f Update client.css 2026-04-07 21:28:49 +02:00
Dome 3255e2ec29 Update client.css 2026-04-07 21:24:23 +02:00
Dome a550178de7 Update main.css 2026-04-07 21:22:26 +02:00
Dome 18284ef0f2 Update main.css 2026-04-07 21:15:53 +02:00
Dome a6b30c1b19 Refactor button styles and layout adjustments
Updated button styles for improved layout and transitions.
2026-04-07 21:08:06 +02:00
Dome 4a41e3f114 Update button styles for consistency and effects
Enhanced button styles with improved transitions and transformations.
2026-04-07 21:01:36 +02:00
Dome 106bece811 Update main.css 2026-04-07 20:53:19 +02:00
Dome ad022b9472 Update main.css 2026-04-07 20:45:31 +02:00
Dome da2f07783d Update main.css 2026-04-07 20:38:54 +02:00
Dome d11fd87696 Reduce margin for button styling 2026-04-07 20:31:35 +02:00
Dome 7cf278beb1 Adjust button margin in main.css 2026-04-07 20:23:59 +02:00
Dome ab2b2c940a Add responsive styles for PreviewView component 2026-04-07 20:17:42 +02:00
Dome 3ca9c00725 Refine styles for Store-Buttons in ClientView
Updated comment for clarity and adjusted margin.
2026-04-07 20:16:15 +02:00
Dome edd095efc4 Enhance CSS for mobile responsiveness and layout
Added mobile fixes and improved box-sizing to prevent horizontal scrolling.
2026-04-07 20:15:08 +02:00
Dome 5832cd0449 Add responsive styles for ClientView actions 2026-04-07 20:13:24 +02:00
Dome d821417a3f Update comments and improve instance handling 2026-04-07 19:55:13 +02:00
Dome 8a2bfff1d9 Update Element.js 2026-04-07 19:50:20 +02:00
Dome e6c548611b Delete images/favicon.png 2026-04-07 19:39:39 +02:00
Dome 47b834779a Update index.html 2026-04-07 19:39:19 +02:00
Dome eb0a7d4522 Update index.html 2026-04-07 19:36:20 +02:00
Dome 61ce605627 Add files via upload 2026-04-07 19:35:20 +02:00
Dome cda2271e9e Reload page after clearing preferences 2026-04-07 19:32:44 +02:00
Dome ef43b377c1 Update RootView.js 2026-04-07 19:29:21 +02:00
Dome 5605404bae Update RootView.js 2026-04-07 19:18:46 +02:00
Dome 211b14db78 Update RootView.js 2026-04-07 19:16:16 +02:00
Dome 160ddead30 Update main.css 2026-04-07 18:53:08 +02:00
Dome 2a3f7f2dec Update client.css 2026-04-07 18:44:47 +02:00
Dome 52dbfeadda Update preview.css 2026-04-07 18:44:27 +02:00
Dome 876b4c742c Update main.css 2026-04-07 18:44:10 +02:00
Dome 12040df452 Update client.css 2026-04-07 17:42:54 +02:00
Dome 80c28bc155 Refine avatar styles and update text appearance
Updated avatar styles and adjusted dimensions for a more modern look. Improved placeholder animations and refined text styles for better readability.
2026-04-07 17:42:40 +02:00
Dome dbae95dca4 Revamp CSS variables and styles for improved design 2026-04-07 17:42:25 +02:00
Dome 45681069cd Merge pull request #3 from matrix-org/main
sync to upstream
2026-04-07 17:28:24 +02:00
Michael Telatynski de7992d60c Merge pull request #394 from vorburger/patch-1 2026-01-02 14:58:25 +00:00
Michael Vorburger 88a7eb03a7 Update README.md
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-01-02 14:57:19 +01:00
Michael Vorburger 3474e341a5 docs: Simplified clarification re. Spaces on README
Based on https://github.com/matrix-org/matrix.to/pull/394#discussion_r2425555065 feedback.
2025-10-13 18:14:53 +02:00
Michael Vorburger 8e82c4687e docs: Clarify matrix.to URL scheme for Spaces on README (fixes #393) 2025-10-12 01:04:30 +02:00
Matthew Hodgson e8fcc3d2b9 Merge pull request #380 from bartvdbraak/patch-1
Add Blender Chat to Trusted Instances
2025-09-17 23:36:28 +01:00
Travis Ralston 2b44055047 Merge pull request #384 from matrix-org/travis/v12-v2
Fix capture group index on non-v12 rooms
2025-07-16 12:32:01 -06:00
Travis Ralston f8c30eabde regex is hard 2025-07-16 12:30:19 -06:00
Travis Ralston 3e521505c0 Merge pull request #383 from matrix-org/travis/v12
Support v12 room IDs
2025-07-16 12:24:39 -06:00
Travis Ralston 442ed2f1d1 Support v12 room IDs
MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4291
Pre-disclosure: https://matrix.org/blog/2025/07/security-predisclosure/
2025-07-16 12:09:19 -06:00
Bart van der Braak 26020e3e95 Add Blender Chat to Trusted Instances 2025-07-09 13:04:41 +02:00
Dome 28d77a898c Merge pull request #2 from matrix-org/main
Change the Element mobile download links to Element X. (#377)
2025-04-17 17:29:38 +02:00
Doug 5b4b57306b Change the Element mobile download links to Element X. (#377)
* Change the Element mobile download links to Element X.

* retrigger checks

---------

Co-authored-by: David Langley <davidl@element.io>
2025-04-11 13:48:02 +01:00
Dome 2e4ce08874 Update Element.js 2025-03-29 00:42:39 +01:00
Dome b49724d83b Update Element.js 2025-03-29 00:35:20 +01:00
Dome fe21222d33 Merge pull request #1 from luelista/custom-web-instances
Custom web instances
2025-03-29 00:19:32 +01:00
Dome f1d33f3f63 Update README.md 2025-03-28 16:37:55 +01:00
Dome 1205705555 Update compose.yaml 2025-03-28 16:37:41 +01:00
Dome 4259af79bd Update README.md 2025-03-28 16:36:58 +01:00
Dome 05ebb904b2 Add files via upload 2025-03-28 16:24:32 +01:00
Dome 73200e8c27 Update compose.yaml 2025-03-28 16:23:32 +01:00
Dome 27ff8f19c7 Update Dockerfile 2025-03-28 16:23:19 +01:00
Dome 8124698775 Update README.md 2025-03-28 16:22:52 +01:00
Dome 58890a7c9d Update Dockerfile 2025-03-28 14:52:15 +01:00
Dome 521b2adf67 Update Dockerfile 2025-03-28 14:49:27 +01:00
Dome 84db9ecc9e Update Dockerfile 2025-03-28 14:48:24 +01:00
Dome b75ca64d22 Update README.md 2025-03-28 14:43:32 +01:00
Dome 8fee384fc0 Update compose.yaml 2025-03-28 14:43:16 +01:00
Dome 8c2db8fdc5 Update README.md 2025-03-28 14:41:22 +01:00
Dome bda105ceeb Update compose.yaml 2025-03-28 14:41:04 +01:00
Dome 4e4e034db2 Update Dockerfile 2025-03-28 14:40:47 +01:00
Dome b8047a7988 Update Dockerfile 2025-03-28 14:34:21 +01:00
Dome ef85f6782d Update Dockerfile 2025-03-28 14:33:46 +01:00
Dome 46a14f0e46 Update Dockerfile 2025-03-28 14:29:04 +01:00
Dome ed1230eee8 Update Dockerfile 2025-03-28 14:24:39 +01:00
Dome 334393b9de Update Dockerfile 2025-03-28 14:22:19 +01:00
Dome 684f3f70dd Update Dockerfile 2025-03-28 14:18:21 +01:00
Dome 70b5a4979d Update Dockerfile 2025-03-28 14:04:49 +01:00
Dome 1898808f79 Update README.md 2025-03-28 13:51:57 +01:00
Dome df2c426d76 Update compose.yaml 2025-03-28 13:49:34 +01:00
Dome f04e9484e7 Update Dockerfile 2025-03-28 13:45:30 +01:00
Dome 3fa1ad7269 Update README.md 2025-03-28 13:44:59 +01:00
Mira Nord 6dd9a0213c Trim whitespace and protocol / path information 2025-03-16 21:40:52 +01:00
Mira Nord d993157cfa Add form to configure custom web instance 2025-03-16 21:36:47 +01:00
Mira Nord 06237b1b8b Add link to change custom web instance 2025-03-16 20:55:20 +01:00
Mira Nord 50a25dd04c Add preference for custom web instances, use it for Element 2025-03-16 20:54:38 +01:00
Mira Nord 22ad0e9289 Improve domain splitting for "Hosted by" header 2025-03-16 19:16:06 +01:00
13 changed files with 490 additions and 370 deletions
+6 -4
View File
@@ -2,17 +2,19 @@
FROM node:20.2-alpine AS build
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production && yarn cache clean
RUN yarn install
COPY . .
RUN yarn build
# Stage 2: Production
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
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
EXPOSE 443
# Healthcheck
HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
+4 -3
View File
@@ -56,6 +56,8 @@ The matrix.to URL scheme is
The #/ component is mandatory and exists to avoid leaking the target URL to the
server hosting matrix.to.
There is no _Entity type_ for **Spaces**, as they are technically just rooms.
Note that linking to rooms by ID should only be used for rooms to which the
target user has been invited: these links cannot be assumed to work for all
visitors.
@@ -97,8 +99,7 @@ services:
container_name: Matrix-to
image: domoel/matrix-to:latest
ports:
- "5000:5000"
restart: unless-stopped
- "1336:80"
environment:
- PORT=5000
- NODE_ENV=production
```
+3 -9
View File
@@ -1,16 +1,10 @@
version: '3.8'
services:
web:
matrix-to:
container_name: Matrix-to
image: domoel/matrix-to:latest
ports:
- "80:80"
- "1336:80"
environment:
- NODE_ENV=production
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:80"]
interval: 30s
timeout: 10s
retries: 3
+83 -71
View File
@@ -1,98 +1,110 @@
.ClientListView h2 {
text-align: center;
margin: 18px 0;
}
.ClientListView .filterOption {
display: flex;
align-items: center;
margin: 8px 0;
margin: 24px 0;
font-weight: 600;
}
.ClientView {
border: 1px solid #E6E6E6;
border-radius: 8px;
background: var(--app-background);
border: 1px solid var(--borders);
border-radius: 4px;
margin: 16px 0;
padding: 16px;
padding: 24px;
transition: all 0.3s ease;
border-left: 4px solid transparent;
display: block;
text-align: left;
}
.ClientView:hover {
background: #2f313d !important;
border-left-color: var(--ztfr-purple) !important;
transform: translateX(5px);
}
.ClientView.isPreferred {
border: 3px solid var(--link);
box-shadow: 0px 8px 4px rgba(0, 0, 0, 0.05);
border-color: var(--ztfr-purple);
background: rgba(189, 147, 249, 0.05);
}
.ClientView .hostedBanner {
text-align: center;
margin-bottom: 29px;
padding: 4px 0;
line-height: 20px;
border-radius: 8px;
margin-bottom: 20px;
padding: 6px 0;
border-radius: 4px;
font-weight: bold;
font-size: 16px;
background-color: var(--lightgrey);
font-size: 13px;
background-color: var(--ztfr-purple);
color: var(--app-background);
text-transform: uppercase;
}
.ClientView .header {
display: flex;
}
.ClientView .description {
flex: 1;
}
.ClientView h3 {
margin-top: 0;
}
.ClientView .header { display: flex; align-items: flex-start; }
.ClientView .description { flex: 1; }
.ClientView h3 { margin: 0 0 8px 0; font-size: 18px; }
.ClientView .description p { margin: 0; font-size: 13px; color: var(--font); }
.ClientView .clientIcon {
border-radius: 8px;
background-repeat: no-repeat;
background-size: cover;
width: 60px;
height: 60px;
overflow: hidden;
display: block;
border-radius: 4px;
background-color: #ffffff;
padding: 4px;
width: 50px;
height: 50px;
margin-left: 16px;
flex-shrink: 0;
}
.ClientView .platforms {
background-image: url('../images/platform-icon.svg');
background-repeat: no-repeat;
background-position: 0 center;
padding-left: 28px;
}
.ClientView .actions a.badge {
display: inline-block;
height: 40px;
margin: 8px 16px 8px 0;
}
.ClientView .actions img {
height: 100%;
}
.ClientView .back {
margin-top: 22px;
}
.InstallClientView .instructions button {
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
padding: 4px;
border: none;
width: 24px;
height: 24px;
margin: 8px;
vertical-align: middle;
}
.InstallClientView .instructions button.copy {
background-image: url('../images/copy.svg');
margin-top: 12px;
font-size: 12px;
color: var(--grey);
opacity: 0.8;
}
.InstallClientView .instructions button.tick {
background-image: url('../images/tick-dark.svg');
filter: invert(1); /* Macht schwarze Icons weiß */
}
.ClientView .back {
display: block;
margin-top: 20px;
font-size: 12px;
color: var(--grey);
}
.ClientView .actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.ClientView .actions a.badge {
height: auto !important; /* Erlaubt dem Bild, zu skalieren */
max-width: 100%; /* Verhindert das Rausragen */
display: block;
}
.ClientView .actions img {
max-width: 160px; /* Begrenzt die Breite der Store-Buttons (Google Play / F-Droid) */
height: auto;
display: block;
margin: 5px auto;
}
@media screen and (max-width: 480px) {
.ClientView {
padding: 15px; /* Weniger Padding mobil */
margin: 8px 0;
}
.ClientView .header {
flex-direction: column; /* Stapelt Icon und Text mobil untereinander */
align-items: center;
text-align: center;
}
.ClientView .clientIcon {
margin: 0 0 15px 0; /* Icon zentrieren */
}
}
+158 -158
View File
@@ -1,17 +1,6 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Modified 2026 for Zeitfresser Matrix Community Look & Mobile Fixes
*/
@import url('spinner.css');
@@ -20,57 +9,63 @@ limitations under the License.
@import url('create.css');
@import url('open.css');
/* Globaler Fix für Box-Berechnungen */
* {
box-sizing: border-box;
}
:root {
--app-background: #f4f4f4;
--background: #ffffff;
--foreground: #000000;
--font: #333333;
--grey: #666666;
--accent: #0098d4;
--error: #d6001c;
--link: #0098d4;
--borders: #f4f4f4;
--lightgrey: #E6E6E6;
--app-background: #1e1f29; /* Tief-Anthrazit der Webseite */
--background: #282a36; /* Hintergrund der Card */
--foreground: #f7f7fa; /* Reines Highlight-Weiß */
--font: #bdc3c7; /* Fließtext-Grau */
--grey: #64748b; /* Dunkles Grau für Identifier */
--accent: #0dbd8b; /* Matrix-Green */
--ztfr-purple: #bd93f9; /* Das Zeitfresser-Lila Akzent */
--error: #ff5555;
--link: #f7f7fa;
--borders: #2f313d;
--lightgrey: #383a59;
--spinner-stroke-size: 2px;
--sub-text: #bdc3c7;
}
html {
margin: 0;
padding: 0;
height: 100%;
overflow-x: hidden;
}
body {
background-color: var(--app-background);
background-image: url('../images/background.svg');
background-attachment: fixed;
background-repeat: no-repeat;
background-size: auto;
background-position: center -50px;
height: 100%;
width: 100%;
font-size: 14px;
background-image: none !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: var(--font);
padding: 120px 0 0 0;
padding: 80px 20px 0 20px;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
noscript {
display: block;
padding: 20px;
h1, h2, h3 {
color: var(--foreground);
letter-spacing: -0.5px;
word-wrap: break-word;
}
p { line-height: 150%; }
a { text-decoration: none; }
p {
line-height: 1.7;
word-wrap: break-word;
}
h1 { font-size: 24px; }
h2 { font-size: 21px; }
h3 { font-size: 16px; }
a { text-decoration: none; color: var(--link); }
body,
button,
input,
textarea {
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
body, button, input, textarea {
font-size: 14px;
font-style: normal;
}
@@ -78,13 +73,14 @@ button, input[type=submit] {
cursor: pointer;
}
button, input {
font-size: inherit;
font-weight: inherit;
}
input[type="checkbox"], input[type="radio"] {
margin: 0 8px 0 0;
/* Die zentrale Kachel */
.card {
background-color: var(--background);
border-radius: 4px;
border-left: 4px solid var(--ztfr-purple);
box-shadow: 0px 20px 40px rgba(0, 0, 0, 0.3);
padding: 2.5rem;
width: 100%;
}
.RootView {
@@ -93,131 +89,135 @@ input[type="checkbox"], input[type="radio"] {
width: 100%;
}
.card {
background-color: var(--background);
border-radius: 16px;
box-shadow: 0px 18px 24px rgba(0, 0, 0, 0.06);
}
.card, .footer {
padding: 2rem;
}
.hidden {
display: none !important;
}
@media screen and (max-width: 480px) {
body {
background-image: none;
background-color: var(--background);
padding: 0;
}
.card {
border-radius: unset;
box-shadow: unset;
}
}
.footer .links li:not(:first-child) {
margin-left: 0.5em;
}
.footer .links li:not(:first-child)::before {
content: "·";
margin-right: 0.5em;
}
.footer .links li {
display: inline-block;
}
.footer .links {
font-size: 12px;
list-style: none;
padding: 0;
}
a, button.text {
color: var(--link);
}
button.text {
background: none;
border: none;
font-style: normal;
font-weight: normal;
font-size: inherit;
padding: 8px 0;
margin: -8px 0;
}
button.text:hover {
cursor: pointer;
}
/* Buttons: Eckig & Kontrastreich */
.primary, .secondary {
text-decoration: none;
font-weight: bold;
text-align: center;
padding: 12px 8px;
margin: 8px 0;
}
.secondary {
background: var(--background);
color: var(--link);
border: 1px solid var(--link);
border-radius: 32px;
padding: 14px 20px;
margin: 4px 0 !important;
display: block;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.2s ease;
width: 100%;
}
.primary {
background: var(--link);
color: var(--background);
border-radius: 32px;
}
.primary.icon, .secondary.icon {
background-repeat: no-repeat;
background-position: 12px center;
}
.icon.link { background-image: url('../images/link.svg'); }
.icon.tick { background-image: url('../images/tick.svg'); }
.icon.copy { background-image: url('../images/copy.svg'); }
button.primary, input[type='submit'].primary, button.secondary, input[type='submit'].secondary {
background: var(--foreground);
color: var(--app-background) !important;
border-radius: 4px;
border: none;
font-size: inherit;
}
.primary:hover {
filter: brightness(0.9);
}
.secondary {
background: transparent;
color: var(--foreground);
border: 1px solid var(--foreground);
border-radius: 4px;
}
.secondary:hover {
background: rgba(255,255,255,0.05);
}
/* Fix für Button-Container (entfernt versteckte Gaps) */
.actions,
.ClientView .actions,
.ClientListView .actions,
.CustomInstanceView .actions {
display: flex !important;
flex-direction: column !important;
gap: 0px !important;
}
/* Entfernt den Abstand beim letzten Button in einer Gruppe für saubere Optik */
.actions > *:last-child {
margin-bottom: 0 !important;
}
/* Input Felder: Dunkel statt Weiß */
input[type='text'].large {
width: 100%;
padding: 12px;
background: var(--background);
border: 1px solid var(--foreground);
border-radius: 16px;
padding: 14px;
background: #1a1a1a !important;
border: 1px solid var(--borders);
border-radius: 4px;
font-size: 14px;
}
.fullwidth {
display: block;
width: 100%;
color: var(--foreground);
box-sizing: border-box;
margin-bottom: 10px;
}
.LoadServerPolicyView {
display: flex;
.footer {
margin-top: 60px;
padding: 40px 20px;
text-align: center;
font-size: 0.85rem;
color: var(--sub-text);
opacity: 0.7;
border-top: 1px solid rgba(255, 255, 255, 0.05);
width: 100%;
}
.LoadServerPolicyView .spinner {
width: 32px;
height: 32px;
margin-right: 12px;
.footer ul {
list-style: none;
padding: 0;
margin: 0;
}
.LoadServerPolicyView h2 {
margin-top: 0;
.footer li {
display: inline;
margin: 0;
}
.footer a {
color: inherit;
text-decoration: none;
border-bottom: 1px solid rgba(189, 195, 199, 0.3);
transition: all 0.3s ease;
}
.footer a:hover {
color: var(--foreground) !important;
border-bottom-color: var(--ztfr-purple) !important;
}
.footer button,
.footer input[type="submit"],
.ClientListView button.change,
.PreviewView button.change {
background: none !important;
border: none !important;
color: var(--grey) !important;
padding: 0 !important;
font-family: inherit !important;
font-size: inherit !important;
cursor: pointer !important;
border-bottom: 1px solid rgba(189, 195, 199, 0.3) !important;
}
.footer button:hover {
color: var(--foreground) !important;
border-bottom-color: var(--ztfr-purple) !important;
}
/* SPEZIELLE MOBILE ANPASSUNGEN */
@media screen and (max-width: 480px) {
body {
padding: 40px 10px 0 10px;
}
.card {
padding: 1.5rem;
border-radius: 2px;
}
h1 {
font-size: 1.5rem;
}
}
+54 -89
View File
@@ -4,130 +4,95 @@
}
.PreviewView h1 {
font-size: 24px;
line-height: 32px;
font-size: 26px;
line-height: 1.2;
margin-bottom: 8px;
word-wrap: anywhere;
font-weight: 700;
}
.PreviewView .avatarContainer {
display: flex;
justify-content: center;
margin: 0;
margin-bottom: 20px;
}
.PreviewView .avatar {
border-radius: 100%;
width: 64px;
height: 64px;
}
.PreviewView .mxSpace .avatar {
border-radius: 12px;
border-radius: 8px; /* Eckig */
width: 80px;
height: 80px;
border: 3px solid var(--borders);
object-fit: cover;
}
.PreviewView .defaultAvatar {
width: 64px;
height: 64px;
width: 80px;
height: 80px;
border-radius: 8px;
background-color: var(--lightgrey);
background-image: url('../images/chat-icon.svg');
background-repeat: no-repeat;
background-position: center;
background-size: 85%;
}
.PreviewView .spinner {
width: 32px;
height: 32px;
}
.PreviewView .avatar.loading {
border: 1px solid #eee;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
opacity: 0.6;
}
.PreviewView .identifier {
color: var(--grey);
font-size: 12px;
color: var(--ztfr-purple);
font-family: monospace;
font-size: 13px;
margin: 8px 0;
}
.PreviewView .identifier.placeholder {
height: 1em;
margin: 1em 30%;
}
.PreviewView .memberCount {
display: flex;
justify-content: center;
margin: 8px 0;
}
.PreviewView .memberCount.loading {
margin: 16px 0;
}
.PreviewView .memberCount p {
font-size: 12px;
margin: 0;
letter-spacing: 0.5px;
}
.PreviewView .memberCount p:not(.placeholder) {
padding: 4px 8px 4px 24px;
border-radius: 14px;
padding: 6px 12px 6px 28px;
border-radius: 4px;
color: var(--foreground);
background-image: url(../images/member-icon.svg);
background-repeat: no-repeat;
background-position: 2px center;
background-position: 8px center;
background-color: var(--lightgrey);
}
.PreviewView .memberCount p.placeholder {
height: 1.5em;
width: 100px;
display: inline-block;
font-size: 12px;
}
.PreviewView .topic {
font-size: 12px;
color: var(--grey);
margin: 32px 0;
}
.PreviewView .topic.loading {
display: block;
margin: 24px 12px;
padding: 4px 0;
}
.PreviewView .topic.loading .placeholder {
height: 0.8em;
display: block;
margin: 12px 0;
}
.PreviewView .topic.loading .placeholder:nth-child(2) {
margin-left: 5%;
margin-right: 5%;
font-size: 14px;
line-height: 1.6;
color: var(--font);
margin: 24px 0;
}
/* Dark Mode Placeholders */
.placeholder {
border-radius: 1em;
--flash-bg: #ddd;
--flash-fg: #eee;
background: linear-gradient(120deg,
var(--flash-bg),
var(--flash-bg) 10%,
var(--flash-fg) calc(10% + 25px),
var(--flash-bg) calc(10% + 50px)
);
border-radius: 4px;
--flash-bg: #2f313d;
--flash-fg: #383a59;
background: linear-gradient(120deg, var(--flash-bg), var(--flash-bg) 10%, var(--flash-fg) calc(10% + 25px), var(--flash-bg) calc(10% + 50px));
animation: flash 2s linear infinite;
background-size: 200%;
}
@keyframes flash {
0% { background-position-x: 0; }
50% { background-position-x: -80%; }
51% { background-position-x: 80%; }
100% { background-position-x: 0%; }
100% { background-position-x: -200%; }
}
.PreviewView h1,
.PreviewView .identifier,
.PreviewView .topic {
word-wrap: break-word; /* Bricht extrem lange Wörter/IDs um */
overflow-wrap: break-word;
max-width: 100%;
}
@media screen and (max-width: 480px) {
.PreviewView .avatar,
.PreviewView .defaultAvatar {
width: 64px;
height: 64px;
}
.PreviewView h1 {
font-size: 20px; /* Titel etwas dezenter mobil */
}
}
+34
View File
@@ -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;
}
}
}
+11 -4
View File
@@ -18,7 +18,7 @@ import {createEnum} from "./utils/enum.js";
import {orderedUnique} from "./utils/unique.js";
const ROOMALIAS_PATTERN = /^#([^:]*):(.+)$/;
const ROOMID_PATTERN = /^!([^:]*):(.+)$/;
const ROOMID_PATTERN = /^!([^:]*)(:(.+))?$/; // As of room version 12, room IDs don't have domains
const USERID_PATTERN = /^@([^:]+):(.+)$/;
const EVENTID_PATTERN = /^$([^:]+):(.+)$/;
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
@@ -152,7 +152,7 @@ export class Link {
}
matches = ROOMID_PATTERN.exec(identifier);
if (matches) {
const server = matches[2];
const server = matches[3]; // group 2 is an optional over `:domain`, group 3 is just `domain`
const localPart = matches[1];
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
}
@@ -166,12 +166,19 @@ export class Link {
}
constructor(clientId, viaServers, identifierKind, localPart, server, webInstances, eventId) {
const servers = [server];
const servers = [];
if (server !== undefined) {
servers.push(server); // v12 rooms don't have domains, and therefore no server
}
servers.push(...viaServers);
this.webInstances = webInstances;
this.servers = orderedUnique(servers);
this.identifierKind = identifierKind;
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
if (identifierKind === IdentifierKind.RoomId && !server) {
this.identifier = `${asPrefix(identifierKind)}${localPart}`;
} else {
this.identifier = `${asPrefix(identifierKind)}${localPart}:${server}`;
}
this.eventId = eventId;
this.clientId = clientId;
}
+18 -1
View File
@@ -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) {
@@ -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) {
@@ -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;
}
}
+8 -8
View File
@@ -30,14 +30,14 @@ export class RootView extends TemplateView {
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
t.div({className: "footer"}, [
t.p(t.img({src: "images/matrix-logo.svg"})),
t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]),
t.ul({className: "links"}, [
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")),
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")),
t.li({className: {hidden: vm => !vm.hasPreferences}},
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
t.li(t.a({href: "#/disclaimer/"}, "Disclaimer")),
t.p([
"© 2026 ",
t.a({href: "https://ztfr.eu"}, "Zeitfresser"),
" | Powered by ",
externalLink(t, "https://github.com/matrix-org/matrix.to", "Matrix-to")
]),
t.p({className: {hidden: vm => !vm.hasPreferences}}, [
t.button({className: "text", onClick: () => { vm.clearPreferences(); location.reload(); }}, "Clear preferences")
])
])
]);
+47
View File
@@ -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"}, [
@@ -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"),
])
]);
}
+46 -13
View File
@@ -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();
}
@@ -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,
@@ -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
});
@@ -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,
@@ -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}`;
}
@@ -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() {
@@ -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;
@@ -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();
}
}
+16 -8
View File
@@ -18,13 +18,15 @@ import {Maturity, Platform, LinkKind,
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
const trustedWebInstances = [
"app.element.io", // first one is the default one
"chat.ztfr.eu", // Zeitfresser ist der gesetzte Standard
"app.element.io",
"develop.element.io",
"chat.fedoraproject.org",
"chat.fosdem.org",
"chat.mozilla.org",
"webchat.kde.org",
"app.gitter.im",
"chat.blender.org",
];
/**
@@ -56,8 +58,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:
@@ -80,12 +83,15 @@ export class Element {
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
if (isWebPlatform || platform === Platform.iOS) {
// Standardmäßig deine Instanz nehmen
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];
// Falls der Nutzer über den "Change"-Dialog eine bevorzugte Instanz
// oder eine Custom-URL gewählt hat, nutzen wir diese:
if (isWebPlatform && preferredWebInstance) {
instanceHost = preferredWebInstance;
}
return `https://${instanceHost}/#/${fragmentPath}`;
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
return `element://vector/webapp/#/${fragmentPath}`;
@@ -98,8 +104,8 @@ export class Element {
getCopyString(platform, link) {}
getInstallLinks(platform) {
switch (platform) {
case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')];
case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')];
case Platform.iOS: return [new AppleStoreLink('element-x-secure-chat-call', 'id1631335820')];
case Platform.Android: return [new PlayStoreLink('io.element.android.x'), new FDroidLink('io.element.android.x')];
default: return [new WebsiteLink("https://element.io/download")];
}
}
@@ -109,6 +115,8 @@ export class Element {
}
getPreferredWebInstance(link) {
// Hier geben wir dem System die Erlaubnis, gespeicherte Präferenzen zu finden.
// Wenn keine da sind, greift oben automatisch trustedWebInstances[0].
const idx = trustedWebInstances.indexOf(link.webInstances[this.id])
return idx === -1 ? undefined : trustedWebInstances[idx];
}