feat: Einzelne Nachrichten als Link teilen

Backend:
- SharedMessageRecord Model + shared_messages DB-Tabelle
- SharedEndpoint: POST /api/messages/{id}/share, GET /api/shared/{token},
  GET /api/shared (Liste), DELETE /api/shared/{token}
- Auth-Ausnahme für GET /api/shared/{token} (öffentlich ohne Login)
- DB-Methoden: CreateSharedMessage, GetSharedMessage, GetChainMessagesUpTo

Frontend:
- Share-Button (fa-share-alt) an jeder Nachricht
- Kontextmenü: Einzelnachricht oder mit Thread-Kontext teilen
- Link wird automatisch in Zwischenablage kopiert
- Öffentliche Shared-Page (#shared/{token}) ohne Login
- Markdown-Rendering in der geteilten Ansicht
- CSS für Share-Button, Share-Menü und Shared-Page
This commit is contained in:
Meleki committed 2026-03-04 23:34:00 +01:00
1 parent 4e915f76af
commit be29984b25
12 files changed
+591 -8

No files matched your search

+119 -4
View File
@@ -414,7 +414,8 @@
.msg-reply-btn,
.msg-delete-btn,
.msg-mark-btn {
.msg-mark-btn,
.msg-share-btn {
position: absolute;
top: var(--badge-spacing);
opacity: 0;
@@ -428,20 +429,25 @@
}
.msg-reply-btn {
right: calc(var(--badge-spacing) + 4em + var(--badge-spacing));
right: calc(var(--badge-spacing) + 6em + var(--badge-spacing) * 1.5);
}
.msg-delete-btn {
right: calc(var(--badge-spacing) + 2em + var(--badge-spacing) / 2);
right: calc(var(--badge-spacing) + 4em + var(--badge-spacing));
}
.msg-mark-btn {
right: calc(var(--badge-spacing) + 2em + var(--badge-spacing) / 2);
}
.msg-share-btn {
right: var(--badge-spacing);
}
.msg:hover .msg-reply-btn,
.msg:hover .msg-delete-btn,
.msg:hover .msg-mark-btn {
.msg:hover .msg-mark-btn,
.msg:hover .msg-share-btn {
opacity: 1;
}
@@ -858,3 +864,112 @@
object-fit: contain;
border-radius: var(--border-radius);
}
/* Share Menu */
.share-menu {
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
min-width: 12em;
}
.share-menu-item {
display: flex;
align-items: center;
gap: var(--badge-spacing);
padding: var(--menu-item-padding);
background: none;
border: none;
text-align: left;
white-space: nowrap;
cursor: pointer;
color: var(--text);
font-size: var(--font-size);
}
.share-menu-item:hover {
background: var(--f3-bg);
border: none;
}
/* Shared Page (öffentliche Ansicht) */
.shared-page {
display: flex;
flex-direction: column;
min-height: 100dvh;
max-width: var(--page-width);
margin: 0 auto;
}
.shared-header {
display: flex;
align-items: center;
gap: var(--box-spacing);
padding: var(--page-padding);
border-bottom: var(--border-width) solid var(--f2-border);
}
.shared-logo {
font-weight: bold;
font-size: 1.2em;
letter-spacing: .05em;
}
.shared-label {
opacity: var(--semi-transparent);
font-size: var(--font-size);
}
.shared-content {
flex: 1;
padding: var(--page-padding);
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
overflow-y: auto;
}
.shared-content .msg {
max-width: 100%;
}
.shared-loading,
.shared-error {
text-align: center;
padding: 3em 1em;
opacity: var(--semi-transparent);
}
.shared-error {
color: var(--dangerous);
opacity: 1;
}
.shared-meta {
text-align: center;
padding: var(--box-padding);
opacity: var(--semi-transparent);
font-size: var(--font-size);
}
.shared-footer {
padding: var(--page-padding);
text-align: center;
border-top: var(--border-width) solid var(--f2-border);
}
.shared-cta {
color: var(--primary);
text-decoration: none;
font-weight: bold;
}
.shared-cta:hover {
text-decoration: underline;
}