Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b86b69d171 | |||
| e009496885 | |||
| 690692b494 | |||
| 5df8bdf932 | |||
| 030966c506 | |||
| 5a633a0ebb | |||
| 50d03aad1d | |||
| 29b754c9f8 | |||
| 9c90ede9c4 | |||
| 35bdc7473f | |||
| 3255e2ec29 | |||
| a550178de7 | |||
| 18284ef0f2 | |||
| a6b30c1b19 | |||
| 4a41e3f114 | |||
| 106bece811 | |||
| ad022b9472 | |||
| da2f07783d | |||
| d11fd87696 | |||
| 7cf278beb1 | |||
| ab2b2c940a | |||
| 3ca9c00725 | |||
| edd095efc4 | |||
| 5832cd0449 | |||
| d821417a3f | |||
| 8a2bfff1d9 | |||
| e6c548611b | |||
| 47b834779a | |||
| eb0a7d4522 | |||
| 61ce605627 | |||
| cda2271e9e | |||
| ef43b377c1 | |||
| 5605404bae | |||
| 211b14db78 | |||
| 160ddead30 | |||
| 2a3f7f2dec | |||
| 52dbfeadda | |||
| 876b4c742c | |||
| 12040df452 | |||
| 80c28bc155 | |||
| dbae95dca4 | |||
| 45681069cd | |||
| de7992d60c | |||
| 88a7eb03a7 | |||
| 3474e341a5 | |||
| 8e82c4687e | |||
| e8fcc3d2b9 | |||
| 2b44055047 | |||
| f8c30eabde | |||
| 3e521505c0 | |||
| 442ed2f1d1 | |||
| 26020e3e95 | |||
| 28d77a898c | |||
| 5b4b57306b | |||
| 2e4ce08874 | |||
| b49724d83b | |||
| fe21222d33 | |||
| f1d33f3f63 | |||
| 1205705555 | |||
| 4259af79bd | |||
| 05ebb904b2 | |||
| 73200e8c27 | |||
| 27ff8f19c7 | |||
| 8124698775 | |||
| 58890a7c9d | |||
| 521b2adf67 | |||
| 84db9ecc9e | |||
| b75ca64d22 | |||
| 8fee384fc0 | |||
| 8c2db8fdc5 | |||
| bda105ceeb | |||
| 4e4e034db2 | |||
| b8047a7988 | |||
| ef85f6782d | |||
| 46a14f0e46 | |||
| ed1230eee8 | |||
| 334393b9de | |||
| 684f3f70dd | |||
| 70b5a4979d | |||
| 1898808f79 | |||
| df2c426d76 | |||
| f04e9484e7 | |||
| 3fa1ad7269 | |||
| 66cc550949 | |||
| 3a0359c1b6 | |||
| b560e2dc1e | |||
| 8abc0ce856 | |||
| 6dd9a0213c | |||
| d993157cfa | |||
| 06237b1b8b | |||
| 50a25dd04c | |||
| 22ad0e9289 |
@@ -0,0 +1,29 @@
|
|||||||
|
name: Build and Push Matrix-to Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: # Erlaubt den manuellen Start über das GitHub UI
|
||||||
|
|
||||||
|
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:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: domoel/matrix-to:latest
|
||||||
|
no-cache: true
|
||||||
+18
-14
@@ -1,19 +1,23 @@
|
|||||||
# Verwenden Sie ein Node.js-Image als Basis
|
# Stage 1: Build
|
||||||
FROM node:20-alpine
|
FROM node:20.2-alpine AS build
|
||||||
|
|
||||||
# Setzen Sie das Arbeitsverzeichnis im Container
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Kopieren Sie die package.json und yarn.lock Dateien und installieren Sie die Abhängigkeiten
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install
|
||||||
|
|
||||||
# Kopieren Sie den Rest des Codes in das Arbeitsverzeichnis
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
# Exponieren Sie den Port 5000
|
# Stage 2: Production
|
||||||
EXPOSE 5000
|
FROM nginx:alpine
|
||||||
|
WORKDIR /etc/nginx
|
||||||
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/build .
|
||||||
|
|
||||||
# Starten Sie die Anwendung und setzen Sie die PORT-Umgebungsvariable auf 5000
|
# Expose port 80
|
||||||
ENV PORT=5000
|
EXPOSE 80
|
||||||
CMD ["yarn", "start"]
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
|
||||||
|
|
||||||
|
# Start Nginx server
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ The matrix.to URL scheme is
|
|||||||
The #/ component is mandatory and exists to avoid leaking the target URL to the
|
The #/ component is mandatory and exists to avoid leaking the target URL to the
|
||||||
server hosting matrix.to.
|
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
|
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
|
target user has been invited: these links cannot be assumed to work for all
|
||||||
visitors.
|
visitors.
|
||||||
@@ -97,8 +99,7 @@ services:
|
|||||||
container_name: Matrix-to
|
container_name: Matrix-to
|
||||||
image: domoel/matrix-to:latest
|
image: domoel/matrix-to:latest
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "1336:80"
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
- PORT=5000
|
- NODE_ENV=production
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ services:
|
|||||||
container_name: Matrix-to
|
container_name: Matrix-to
|
||||||
image: domoel/matrix-to:latest
|
image: domoel/matrix-to:latest
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "1336:80"
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
- PORT=5000
|
- NODE_ENV=production
|
||||||
|
|||||||
+106
-71
@@ -1,98 +1,133 @@
|
|||||||
.ClientListView h2 {
|
.ClientListView h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 18px 0;
|
margin: 24px 0;
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
.ClientListView .filterOption {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClientView {
|
.ClientView {
|
||||||
border: 1px solid #E6E6E6;
|
background: var(--app-background);
|
||||||
border-radius: 8px;
|
border: 1px solid var(--borders);
|
||||||
|
border-radius: 4px;
|
||||||
margin: 16px 0;
|
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 {
|
.ClientView.isPreferred {
|
||||||
border: 3px solid var(--link);
|
border-color: var(--ztfr-purple);
|
||||||
box-shadow: 0px 8px 4px rgba(0, 0, 0, 0.05);
|
background: rgba(189, 147, 249, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClientView .hostedBanner {
|
.ClientView .hostedBanner {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 29px;
|
margin-bottom: 20px;
|
||||||
padding: 4px 0;
|
padding: 6px 0;
|
||||||
line-height: 20px;
|
border-radius: 4px;
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
background-color: var(--lightgrey);
|
background-color: var(--ztfr-purple);
|
||||||
|
color: var(--app-background);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClientView .header {
|
.ClientView .header { display: flex; align-items: flex-start; }
|
||||||
display: flex;
|
.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 .description {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ClientView h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ClientView .clientIcon {
|
.ClientView .clientIcon {
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
background-repeat: no-repeat;
|
background-color: #ffffff;
|
||||||
background-size: cover;
|
padding: 4px;
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
overflow: hidden;
|
|
||||||
display: block;
|
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ClientView .platforms {
|
.ClientView .platforms {
|
||||||
background-image: url('../images/platform-icon.svg');
|
margin-top: 12px;
|
||||||
background-repeat: no-repeat;
|
font-size: 12px;
|
||||||
background-position: 0 center;
|
color: var(--grey);
|
||||||
padding-left: 28px;
|
opacity: 0.8;
|
||||||
}
|
|
||||||
|
|
||||||
.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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.InstallClientView .instructions button.tick {
|
.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;
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ClientView .actions img {
|
||||||
|
max-width: 160px;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ANPASSUNG FÜR DEN DEZENTEN LOOK --- */
|
||||||
|
|
||||||
|
.ClientView .actions .footer {
|
||||||
|
display: block !important; /* Kein Flex mehr, damit Links wie Text fließen */
|
||||||
|
text-align: center !important;
|
||||||
|
margin-top: 20px !important;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.8 !important; /* Gibt den Zeilen etwas Platz */
|
||||||
|
color: var(--sub-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der "Use Custom Web Instance" Bereich bekommt eine eigene Zeile */
|
||||||
|
.ClientView .actions .footer .custom {
|
||||||
|
display: block !important;
|
||||||
|
margin-top: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sicherstellen, dass der "Change" Button im Textfluss bleibt */
|
||||||
|
.ClientView .actions .footer button.change {
|
||||||
|
display: inline !important;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.ClientView {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ClientView .header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ClientView .clientIcon {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+168
-157
@@ -1,17 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
Modified 2026 for Zeitfresser Matrix Community Look & Mobile Fixes
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('spinner.css');
|
@import url('spinner.css');
|
||||||
@@ -20,57 +9,63 @@ limitations under the License.
|
|||||||
@import url('create.css');
|
@import url('create.css');
|
||||||
@import url('open.css');
|
@import url('open.css');
|
||||||
|
|
||||||
|
/* Globaler Fix für Box-Berechnungen */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--app-background: #f4f4f4;
|
--app-background: #1e1f29; /* Tief-Anthrazit */
|
||||||
--background: #ffffff;
|
--background: #282a36; /* Card Hintergrund */
|
||||||
--foreground: #000000;
|
--foreground: #f7f7fa; /* Weiß */
|
||||||
--font: #333333;
|
--font: #bdc3c7; /* Grau */
|
||||||
--grey: #666666;
|
--grey: #64748b; /* Dunkles Grau */
|
||||||
--accent: #0098d4;
|
--accent: #0dbd8b; /* Matrix-Green */
|
||||||
--error: #d6001c;
|
--ztfr-purple: #bd93f9; /* Zeitfresser-Lila */
|
||||||
--link: #0098d4;
|
--error: #ff5555;
|
||||||
--borders: #f4f4f4;
|
--link: #f7f7fa;
|
||||||
--lightgrey: #E6E6E6;
|
--borders: #2f313d;
|
||||||
|
--lightgrey: #383a59;
|
||||||
--spinner-stroke-size: 2px;
|
--spinner-stroke-size: 2px;
|
||||||
|
--sub-text: #bdc3c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--app-background);
|
background-color: var(--app-background);
|
||||||
background-image: url('../images/background.svg');
|
background-image: none !important;
|
||||||
background-attachment: fixed;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: auto;
|
|
||||||
background-position: center -50px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--font);
|
color: var(--font);
|
||||||
padding: 120px 0 0 0;
|
padding: 80px 20px 0 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
noscript {
|
h1, h2, h3 {
|
||||||
display: block;
|
color: var(--foreground);
|
||||||
padding: 20px;
|
letter-spacing: -0.5px;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
p { line-height: 150%; }
|
p {
|
||||||
a { text-decoration: none; }
|
line-height: 1.7;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
h1 { font-size: 24px; }
|
a { text-decoration: none; color: var(--link); }
|
||||||
h2 { font-size: 21px; }
|
|
||||||
h3 { font-size: 16px; }
|
|
||||||
|
|
||||||
body,
|
body, button, input, textarea {
|
||||||
button,
|
font-size: 14px;
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +73,14 @@ button, input[type=submit] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input {
|
/* Die zentrale Kachel */
|
||||||
font-size: inherit;
|
.card {
|
||||||
font-weight: inherit;
|
background-color: var(--background);
|
||||||
}
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid var(--ztfr-purple);
|
||||||
input[type="checkbox"], input[type="radio"] {
|
box-shadow: 0px 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
margin: 0 8px 0 0;
|
padding: 2.5rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.RootView {
|
.RootView {
|
||||||
@@ -93,131 +89,146 @@ input[type="checkbox"], input[type="radio"] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
/* --- GROSSE BUTTONS (z.B. Download, Continue) --- */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary, .secondary {
|
.primary, .secondary {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px 8px;
|
padding: 14px 20px;
|
||||||
margin: 8px 0;
|
margin: 4px 0 !important;
|
||||||
}
|
display: block;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.secondary {
|
/* Simplifizierung: Nur Farbe, keine Bewegung */
|
||||||
background: var(--background);
|
transition: background-color 0.2s ease, filter 0.2s ease;
|
||||||
color: var(--link);
|
transform: none !important;
|
||||||
border: 1px solid var(--link);
|
-webkit-font-smoothing: antialiased;
|
||||||
border-radius: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
background: var(--link);
|
background: var(--foreground);
|
||||||
color: var(--background);
|
color: var(--app-background) !important;
|
||||||
border-radius: 32px;
|
border-radius: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
border: none;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- DEZENTE BUTTONS (Change, Custom Instance, Footer-Style) --- */
|
||||||
|
.ClientListView button.change,
|
||||||
|
.PreviewView button.change,
|
||||||
|
.ClientView button.custom,
|
||||||
|
.ClientView .footer button,
|
||||||
|
.CustomInstanceView .actions button.secondary,
|
||||||
|
.footer button,
|
||||||
|
.footer a {
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
color: var(--grey) !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: normal !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
letter-spacing: normal !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
display: inline !important;
|
||||||
|
width: auto !important;
|
||||||
|
border-bottom: 1px solid rgba(189, 195, 199, 0.3) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ClientListView button.change:hover,
|
||||||
|
.PreviewView button.change:hover,
|
||||||
|
.ClientView button.custom:hover,
|
||||||
|
.ClientView .footer button:hover,
|
||||||
|
.CustomInstanceView .actions button.secondary:hover,
|
||||||
|
.footer button:hover,
|
||||||
|
.footer a:hover {
|
||||||
|
color: var(--foreground) !important;
|
||||||
|
border-bottom-color: var(--ztfr-purple) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix für Button-Container */
|
||||||
|
.actions,
|
||||||
|
.ClientView .actions,
|
||||||
|
.ClientListView .actions,
|
||||||
|
.CustomInstanceView .actions {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > *:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Felder */
|
||||||
input[type='text'].large {
|
input[type='text'].large {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 14px;
|
||||||
background: var(--background);
|
background: #1a1a1a !important;
|
||||||
border: 1px solid var(--foreground);
|
border: 1px solid var(--borders);
|
||||||
border-radius: 16px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
color: var(--foreground);
|
||||||
|
|
||||||
.fullwidth {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadServerPolicyView {
|
.footer {
|
||||||
display: flex;
|
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 {
|
.footer ul { list-style: none; padding: 0; margin: 0; }
|
||||||
width: 32px;
|
.footer li { display: inline; margin: 0; }
|
||||||
height: 32px;
|
|
||||||
margin-right: 12px;
|
@media screen and (max-width: 480px) {
|
||||||
|
body { padding: 40px 10px 0 10px; }
|
||||||
|
.card { padding: 1.5rem; border-radius: 2px; }
|
||||||
|
h1 { font-size: 1.5rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoadServerPolicyView h2 {
|
/* --- FIX FÜR NUTZERPROFILE (null-Werte verstecken) --- */
|
||||||
margin-top: 0;
|
|
||||||
|
.PreviewView .members[data-count="null"],
|
||||||
|
.PreviewView .members:empty {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PreviewView .topic:empty,
|
||||||
|
.PreviewView .topic:contains("null") {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PreviewView p:empty,
|
||||||
|
.PreviewView span:empty {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PreviewView h2 + p {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-89
@@ -4,130 +4,95 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView h1 {
|
.PreviewView h1 {
|
||||||
font-size: 24px;
|
font-size: 26px;
|
||||||
line-height: 32px;
|
line-height: 1.2;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
word-wrap: anywhere;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .avatarContainer {
|
.PreviewView .avatarContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .avatar {
|
.PreviewView .avatar {
|
||||||
border-radius: 100%;
|
border-radius: 8px; /* Eckig */
|
||||||
width: 64px;
|
width: 80px;
|
||||||
height: 64px;
|
height: 80px;
|
||||||
}
|
border: 3px solid var(--borders);
|
||||||
|
object-fit: cover;
|
||||||
.PreviewView .mxSpace .avatar {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .defaultAvatar {
|
.PreviewView .defaultAvatar {
|
||||||
width: 64px;
|
width: 80px;
|
||||||
height: 64px;
|
height: 80px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--lightgrey);
|
||||||
background-image: url('../images/chat-icon.svg');
|
background-image: url('../images/chat-icon.svg');
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: 85%;
|
opacity: 0.6;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .identifier {
|
.PreviewView .identifier {
|
||||||
color: var(--grey);
|
color: var(--ztfr-purple);
|
||||||
font-size: 12px;
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .memberCount p:not(.placeholder) {
|
.PreviewView .memberCount p:not(.placeholder) {
|
||||||
padding: 4px 8px 4px 24px;
|
padding: 6px 12px 6px 28px;
|
||||||
border-radius: 14px;
|
border-radius: 4px;
|
||||||
|
color: var(--foreground);
|
||||||
background-image: url(../images/member-icon.svg);
|
background-image: url(../images/member-icon.svg);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 2px center;
|
background-position: 8px center;
|
||||||
background-color: var(--lightgrey);
|
background-color: var(--lightgrey);
|
||||||
}
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
.PreviewView .memberCount p.placeholder {
|
|
||||||
height: 1.5em;
|
|
||||||
width: 100px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PreviewView .topic {
|
.PreviewView .topic {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
color: var(--grey);
|
line-height: 1.6;
|
||||||
margin: 32px 0;
|
color: var(--font);
|
||||||
}
|
margin: 24px 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%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Placeholders */
|
||||||
.placeholder {
|
.placeholder {
|
||||||
border-radius: 1em;
|
border-radius: 4px;
|
||||||
--flash-bg: #ddd;
|
--flash-bg: #2f313d;
|
||||||
--flash-fg: #eee;
|
--flash-fg: #383a59;
|
||||||
background: linear-gradient(120deg,
|
background: linear-gradient(120deg, var(--flash-bg), var(--flash-bg) 10%, var(--flash-fg) calc(10% + 25px), var(--flash-bg) calc(10% + 50px));
|
||||||
var(--flash-bg),
|
|
||||||
var(--flash-bg) 10%,
|
|
||||||
var(--flash-fg) calc(10% + 25px),
|
|
||||||
var(--flash-bg) calc(10% + 50px)
|
|
||||||
);
|
|
||||||
animation: flash 2s linear infinite;
|
animation: flash 2s linear infinite;
|
||||||
background-size: 200%;
|
background-size: 200%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash {
|
@keyframes flash {
|
||||||
0% { background-position-x: 0; }
|
0% { background-position-x: 0; }
|
||||||
50% { background-position-x: -80%; }
|
100% { background-position-x: -200%; }
|
||||||
51% { background-position-x: 80%; }
|
}
|
||||||
100% { background-position-x: 0%; }
|
|
||||||
|
.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
@@ -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
@@ -18,7 +18,7 @@ import {createEnum} from "./utils/enum.js";
|
|||||||
import {orderedUnique} from "./utils/unique.js";
|
import {orderedUnique} from "./utils/unique.js";
|
||||||
|
|
||||||
const ROOMALIAS_PATTERN = /^#([^:]*):(.+)$/;
|
const ROOMALIAS_PATTERN = /^#([^:]*):(.+)$/;
|
||||||
const ROOMID_PATTERN = /^!([^:]*):(.+)$/;
|
const ROOMID_PATTERN = /^!([^:]*)(:(.+))?$/; // As of room version 12, room IDs don't have domains
|
||||||
const USERID_PATTERN = /^@([^:]+):(.+)$/;
|
const USERID_PATTERN = /^@([^:]+):(.+)$/;
|
||||||
const EVENTID_PATTERN = /^$([^:]+):(.+)$/;
|
const EVENTID_PATTERN = /^$([^:]+):(.+)$/;
|
||||||
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
const GROUPID_PATTERN = /^\+([^:]+):(.+)$/;
|
||||||
@@ -152,7 +152,7 @@ export class Link {
|
|||||||
}
|
}
|
||||||
matches = ROOMID_PATTERN.exec(identifier);
|
matches = ROOMID_PATTERN.exec(identifier);
|
||||||
if (matches) {
|
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];
|
const localPart = matches[1];
|
||||||
return new Link(clientId, viaServers, IdentifierKind.RoomId, localPart, server, webInstances, eventId);
|
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) {
|
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);
|
servers.push(...viaServers);
|
||||||
this.webInstances = webInstances;
|
this.webInstances = webInstances;
|
||||||
this.servers = orderedUnique(servers);
|
this.servers = orderedUnique(servers);
|
||||||
this.identifierKind = identifierKind;
|
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.eventId = eventId;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -25,6 +25,7 @@ export class Preferences extends EventEmitter {
|
|||||||
// used to differentiate web from native if a client supports both
|
// used to differentiate web from native if a client supports both
|
||||||
this.platform = null;
|
this.platform = null;
|
||||||
this.homeservers = null;
|
this.homeservers = null;
|
||||||
|
this.customWebInstances = {};
|
||||||
|
|
||||||
const prefsStr = localStorage.getItem("preferred_client");
|
const prefsStr = localStorage.getItem("preferred_client");
|
||||||
if (prefsStr) {
|
if (prefsStr) {
|
||||||
@@ -36,6 +37,10 @@ export class Preferences extends EventEmitter {
|
|||||||
if (serversStr) {
|
if (serversStr) {
|
||||||
this.homeservers = JSON.parse(serversStr);
|
this.homeservers = JSON.parse(serversStr);
|
||||||
}
|
}
|
||||||
|
const customWebInstancesStr = localStorage.getItem("custom_web_instances");
|
||||||
|
if (customWebInstancesStr) {
|
||||||
|
this.customWebInstances = JSON.parse(customWebInstancesStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setClient(id, platform) {
|
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() {
|
clear() {
|
||||||
this._localStorage.removeItem("preferred_client");
|
this._localStorage.removeItem("preferred_client");
|
||||||
this._localStorage.removeItem("consented_servers");
|
this._localStorage.removeItem("consented_servers");
|
||||||
|
this._localStorage.removeItem("custom_web_instances");
|
||||||
this.clientId = null;
|
this.clientId = null;
|
||||||
this.platform = null;
|
this.platform = null;
|
||||||
this.homeservers = null;
|
this.homeservers = null;
|
||||||
|
this.customWebInstances = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get canClear() {
|
get canClear() {
|
||||||
return !!this.clientId || !!this.platform || !!this.homeservers;
|
return !!this.clientId || !!this.platform || !!this.homeservers || !!this.customWebInstances;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -30,14 +30,14 @@ export class RootView extends TemplateView {
|
|||||||
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
|
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),
|
||||||
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
|
t.mapView(vm => vm.loadServerPolicyViewModel, vm => vm ? new LoadServerPolicyView(vm) : null),
|
||||||
t.div({className: "footer"}, [
|
t.div({className: "footer"}, [
|
||||||
t.p(t.img({src: "images/matrix-logo.svg"})),
|
t.p([
|
||||||
t.p(["This invite uses ", externalLink(t, "https://matrix.org", "Matrix"), ", an open network for secure, decentralized communication."]),
|
"© 2026 ",
|
||||||
t.ul({className: "links"}, [
|
t.a({href: "https://ztfr.eu"}, "Zeitfresser"),
|
||||||
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to", "GitHub project")),
|
" | Powered by ",
|
||||||
t.li(externalLink(t, "https://github.com/matrix-org/matrix.to/tree/main/src/open/clients", "Add your app")),
|
externalLink(t, "https://github.com/matrix-org/matrix.to", "Matrix-to")
|
||||||
t.li({className: {hidden: vm => !vm.hasPreferences}},
|
]),
|
||||||
t.button({className: "text", onClick: () => vm.clearPreferences()}, "Clear preferences")),
|
t.p({className: {hidden: vm => !vm.hasPreferences}}, [
|
||||||
t.li(t.a({href: "#/disclaimer/"}, "Disclaimer")),
|
t.button({className: "text", onClick: () => { vm.clearPreferences(); location.reload(); }}, "Clear preferences")
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ function renderInstructions(parts) {
|
|||||||
export class ClientView extends TemplateView {
|
export class ClientView extends TemplateView {
|
||||||
|
|
||||||
render(t, vm) {
|
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}}, [
|
return t.div({className: {"ClientView": true, "isPreferred": vm => vm.hasPreferredWebInstance}}, [
|
||||||
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
|
... vm.hasPreferredWebInstance ? [t.div({className: "hostedBanner"}, vm.hostedByBannerLabel)] : [],
|
||||||
t.div({className: "header"}, [
|
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) {
|
function showBack(t, vm) {
|
||||||
return t.p({className: {caption: true, "back": true, hidden: vm => !vm.showBack}}, [
|
return t.p({className: {caption: true, "back": true, hidden: vm => !vm.showBack}}, [
|
||||||
`Continue with ${vm.name} · `,
|
`Continue with ${vm.name} · `,
|
||||||
t.button({className: "text", onClick: () => vm.back()}, "Change"),
|
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
@@ -35,6 +35,7 @@ export class ClientViewModel extends ViewModel {
|
|||||||
this._pickClient = pickClient;
|
this._pickClient = pickClient;
|
||||||
// to provide "choose other client" button after calling pick()
|
// to provide "choose other client" button after calling pick()
|
||||||
this._clientListViewModel = null;
|
this._clientListViewModel = null;
|
||||||
|
this.customWebInstanceFormOpen = false;
|
||||||
this._update();
|
this._update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +60,11 @@ export class ClientViewModel extends ViewModel {
|
|||||||
if (this._proposedPlatform === this._nativePlatform) {
|
if (this._proposedPlatform === this._nativePlatform) {
|
||||||
deepLinkLabel = "Open in app";
|
deepLinkLabel = "Open in app";
|
||||||
} else {
|
} else {
|
||||||
deepLinkLabel = `Open on ${this._client.getPreferredWebInstance(this._link)}`;
|
deepLinkLabel = `Open on ${this.preferredWebInstance}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link);
|
const proposedDeepLink = this._client.getDeepLink(this._proposedPlatform, this._link, this.preferredWebInstance);
|
||||||
if (proposedDeepLink) {
|
if (proposedDeepLink) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: deepLinkLabel,
|
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
|
// 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) {
|
if (hasPreferredWebInstance && this._webPlatform && this._proposedPlatform !== this._webPlatform) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: `Open on ${this._client.getPreferredWebInstance(this._link)}`,
|
label: `Open on ${this.preferredWebInstance}`,
|
||||||
url: this._client.getDeepLink(this._webPlatform, this._link),
|
url: this._client.getDeepLink(this._webPlatform, this._link, this.preferredWebInstance),
|
||||||
kind: "open-in-web",
|
kind: "open-in-web",
|
||||||
activated: () => {} // don't persist this choice as we don't persist the preferred web instance, it's in the url
|
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);
|
actions.push(...nativeActions);
|
||||||
}
|
}
|
||||||
if (this._webPlatform) {
|
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) {
|
if (webDeepLink) {
|
||||||
const webLabel = this.hasPreferredWebInstance ?
|
const webLabel = this.hasPreferredWebInstance ?
|
||||||
`Open on ${this._client.getPreferredWebInstance(this._link)}` :
|
`Open on ${this.preferredWebInstance}` :
|
||||||
`Continue in your browser`;
|
`Continue in your browser`;
|
||||||
actions.push({
|
actions.push({
|
||||||
label: webLabel,
|
label: webLabel,
|
||||||
@@ -128,18 +129,26 @@ export class ClientViewModel extends ViewModel {
|
|||||||
return actions;
|
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)
|
// 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() {
|
get hostedByBannerLabel() {
|
||||||
const preferredWebInstance = this._client.getPreferredWebInstance(this._link);
|
if (this.hasPreferredWebInstance) {
|
||||||
if (this._webPlatform && preferredWebInstance) {
|
const preferredWebInstance = this.preferredWebInstance;
|
||||||
let label = preferredWebInstance;
|
let label = preferredWebInstance;
|
||||||
const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf("."));
|
const subDomainIdx = preferredWebInstance.lastIndexOf(".", preferredWebInstance.lastIndexOf(".") - 1);
|
||||||
if (subDomainIdx !== -1) {
|
if (subDomainIdx !== -1) {
|
||||||
label = preferredWebInstance.slice(preferredWebInstance.length - subDomainIdx + 1);
|
label = preferredWebInstance.slice(subDomainIdx + 1);
|
||||||
}
|
}
|
||||||
return `Hosted by ${label}`;
|
return `Hosted by ${label}`;
|
||||||
}
|
}
|
||||||
@@ -188,7 +197,7 @@ export class ClientViewModel extends ViewModel {
|
|||||||
|
|
||||||
get showDeepLinkInInstall() {
|
get showDeepLinkInInstall() {
|
||||||
// we can assume this._nativePlatform as this._clientCanIntercept already checks it
|
// 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() {
|
get availableOnPlatformNames() {
|
||||||
@@ -223,6 +232,10 @@ export class ClientViewModel extends ViewModel {
|
|||||||
return !!this._clientListViewModel;
|
return !!this._clientListViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get supportsCustomWebInstances() {
|
||||||
|
return !!this._client.supportsCustomInstances;
|
||||||
|
}
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
if (this._clientListViewModel) {
|
if (this._clientListViewModel) {
|
||||||
const vm = 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
|
// 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.
|
// all clients rather than having our "change client" click reverted.
|
||||||
this.preferences.setClient(undefined, undefined);
|
this.preferences.setClient(undefined, undefined);
|
||||||
|
this.preferences.setCustomWebInstance(this._client.id, undefined);
|
||||||
this._update();
|
this._update();
|
||||||
this.emitChange();
|
this.emitChange();
|
||||||
vm.showAll();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ import {Maturity, Platform, LinkKind,
|
|||||||
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
|
FDroidLink, AppleStoreLink, PlayStoreLink, WebsiteLink} from "../types.js";
|
||||||
|
|
||||||
const trustedWebInstances = [
|
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",
|
"develop.element.io",
|
||||||
"chat.fedoraproject.org",
|
"chat.fedoraproject.org",
|
||||||
"chat.fosdem.org",
|
"chat.fosdem.org",
|
||||||
"chat.mozilla.org",
|
"chat.mozilla.org",
|
||||||
"webchat.kde.org",
|
"webchat.kde.org",
|
||||||
"app.gitter.im",
|
"app.gitter.im",
|
||||||
|
"chat.blender.org",
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,8 +58,9 @@ export class Element {
|
|||||||
get homepage() { return "https://element.io"; }
|
get homepage() { return "https://element.io"; }
|
||||||
get author() { return "Element"; }
|
get author() { return "Element"; }
|
||||||
getMaturity(platform) { return Maturity.Stable; }
|
getMaturity(platform) { return Maturity.Stable; }
|
||||||
|
get supportsCustomInstances() { return true; }
|
||||||
|
|
||||||
getDeepLink(platform, link) {
|
getDeepLink(platform, link, preferredWebInstance) {
|
||||||
let fragmentPath;
|
let fragmentPath;
|
||||||
switch (link.kind) {
|
switch (link.kind) {
|
||||||
case LinkKind.User:
|
case LinkKind.User:
|
||||||
@@ -80,12 +83,15 @@ export class Element {
|
|||||||
|
|
||||||
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
|
const isWebPlatform = platform === Platform.DesktopWeb || platform === Platform.MobileWeb;
|
||||||
if (isWebPlatform || platform === Platform.iOS) {
|
if (isWebPlatform || platform === Platform.iOS) {
|
||||||
|
// Standardmäßig deine Instanz nehmen
|
||||||
let instanceHost = trustedWebInstances[0];
|
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.
|
// Falls der Nutzer über den "Change"-Dialog eine bevorzugte Instanz
|
||||||
if (isWebPlatform && trustedWebInstances.includes(link.webInstances[this.id])) {
|
// oder eine Custom-URL gewählt hat, nutzen wir diese:
|
||||||
instanceHost = link.webInstances[this.id];
|
if (isWebPlatform && preferredWebInstance) {
|
||||||
|
instanceHost = preferredWebInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://${instanceHost}/#/${fragmentPath}`;
|
return `https://${instanceHost}/#/${fragmentPath}`;
|
||||||
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
|
} else if (platform === Platform.Linux || platform === Platform.Windows || platform === Platform.macOS) {
|
||||||
return `element://vector/webapp/#/${fragmentPath}`;
|
return `element://vector/webapp/#/${fragmentPath}`;
|
||||||
@@ -98,8 +104,8 @@ export class Element {
|
|||||||
getCopyString(platform, link) {}
|
getCopyString(platform, link) {}
|
||||||
getInstallLinks(platform) {
|
getInstallLinks(platform) {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case Platform.iOS: return [new AppleStoreLink('vector', 'id1083446067')];
|
case Platform.iOS: return [new AppleStoreLink('element-x-secure-chat-call', 'id1631335820')];
|
||||||
case Platform.Android: return [new PlayStoreLink('im.vector.app'), new FDroidLink('im.vector.app')];
|
case Platform.Android: return [new PlayStoreLink('io.element.android.x'), new FDroidLink('io.element.android.x')];
|
||||||
default: return [new WebsiteLink("https://element.io/download")];
|
default: return [new WebsiteLink("https://element.io/download")];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +115,8 @@ export class Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPreferredWebInstance(link) {
|
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])
|
const idx = trustedWebInstances.indexOf(link.webInstances[this.id])
|
||||||
return idx === -1 ? undefined : trustedWebInstances[idx];
|
return idx === -1 ? undefined : trustedWebInstances[idx];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user