fix
This commit is contained in:
parent
d2f4e11390
commit
95e62c2d77
17 changed files with 288 additions and 16 deletions
|
|
@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { copyToClipboard } from "../clipboard";
|
||||
import type { CommandResult } from "../types";
|
||||
import { Alert, JsonBlock } from "./ui";
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ export function ActionButton({
|
|||
: result?.details;
|
||||
|
||||
async function copySecret() {
|
||||
await navigator.clipboard.writeText(secretValue);
|
||||
await copyToClipboard(secretValue);
|
||||
setSecretCopied(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
81
cmd/telesrv-admin/web/src/components/AddServerLinkModal.tsx
Normal file
81
cmd/telesrv-admin/web/src/components/AddServerLinkModal.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { Check, Copy, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { copyToClipboard } from "../clipboard";
|
||||
import { Alert, LoadingSurface } from "./ui";
|
||||
|
||||
// AddServerLinkModal shows a ready-made owpg://addserver link for this
|
||||
// exact server (host+port only -- see the Go handler's doc comment for why
|
||||
// name/description/key/DC are deliberately never embedded in it) -- an
|
||||
// operator hands this out (a website button, a QR code, a message
|
||||
// elsewhere) and the desktop/Android client's "Add Server" form opens
|
||||
// pre-filled from it, fetching the rest straight from this server itself.
|
||||
export function AddServerLinkModal({ onClose }: { onClose: () => void }) {
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api.addServerLink()
|
||||
.then((result) => {
|
||||
if (!cancelled) setLink(result.link);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function copy() {
|
||||
if (!link) return;
|
||||
try {
|
||||
await copyToClipboard(link);
|
||||
setCopied(true);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div className="modal-backdrop" role="presentation">
|
||||
<section className="modal command-modal add-server-link-modal" role="dialog" aria-modal="true" aria-label={"Share server"}>
|
||||
<div className="modal-head">
|
||||
<div>
|
||||
<div className="eyebrow">{"Server"}</div>
|
||||
<h2>{"Share server"}</h2>
|
||||
</div>
|
||||
<button className="icon-btn" type="button" onClick={onClose} aria-label={"Close"}><X size={15} /></button>
|
||||
</div>
|
||||
<div className="command-body">
|
||||
<p>{"Share this link (a button, a QR code, a message) so anyone with the OwpenGram client can add this server in one tap. It only carries the address and port -- the client fetches the name, description, and key directly from the server itself, so the link can never be tampered with to point someone at a fake identity for this address."}</p>
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{!link && !error && <LoadingSurface label={"Building link..."} />}
|
||||
{link && (
|
||||
<div className="add-server-link-field">
|
||||
<label className="form-field">
|
||||
<span>{"owpg:// link"}</span>
|
||||
<textarea value={link} readOnly rows={2} onFocus={(event) => event.currentTarget.select()} />
|
||||
</label>
|
||||
<button className="btn primary icon-text" type="button" onClick={() => void copy()}>
|
||||
<Copy size={15} /> {copied ? "Copy again" : "Copy link"}
|
||||
</button>
|
||||
{copied && (
|
||||
<div className="secret-reveal">
|
||||
<div className="secret-reveal-label"><Check size={14} /> {"Copied to clipboard."}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="btn" type="button" onClick={onClose}>{"Close"}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { Check, Copy, X } from "lucide-react";
|
|||
import { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { copyToClipboard } from "../clipboard";
|
||||
import { Alert } from "./ui";
|
||||
|
||||
// CopyBotTokenModal writes a non-system bot's token straight to the
|
||||
|
|
@ -34,7 +35,7 @@ export function CopyBotTokenModal({ botID, onClose }: { botID: number; onClose:
|
|||
setError(result.error || "No token returned.");
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(token);
|
||||
await copyToClipboard(token);
|
||||
setCopied(true);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
|
|
|
|||
|
|
@ -10,18 +10,21 @@ import {
|
|||
Megaphone,
|
||||
MessageSquareText,
|
||||
Settings,
|
||||
Share2,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Smile,
|
||||
Stamp,
|
||||
Users,
|
||||
Zap,
|
||||
Sticker
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { api } from "../api";
|
||||
import { api, errorMessage } from "../api";
|
||||
import { permissionBotVerificationReview, permissionServerManage, permissionVerificationReview, useCan, useThirdPartyVerificationHidden } from "../permissions";
|
||||
import { type Navigate, type RouteState, routeTitle } from "../routing";
|
||||
import { ThemeSwitch } from "../theme";
|
||||
import { AddServerLinkModal } from "./AddServerLinkModal";
|
||||
import { AppLink } from "./AppLink";
|
||||
|
||||
// Compresses a sorted (or unsorted) list of layer numbers into run-length
|
||||
|
|
@ -85,6 +88,29 @@ export function Shell({
|
|||
// sections are granted independently, so one entry can be visible without the other.
|
||||
const canReviewBotVerification = useCan(permissionBotVerificationReview);
|
||||
const canManageServer = useCan(permissionServerManage);
|
||||
const [addServerLinkOpen, setAddServerLinkOpen] = useState(false);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connectError, setConnectError] = useState("");
|
||||
|
||||
// "Connect" is the short-cut version of Share: instead of showing a link to
|
||||
// copy elsewhere, it opens the owpg://addserver link (host+port only --
|
||||
// see the Go handler's doc comment for why nothing else ever goes in it)
|
||||
// right here in this browser, so if an OwpenGram client is registered for
|
||||
// that scheme on this machine, it launches straight into "Add Server"
|
||||
// pre-filled for the server this very admin panel manages, fetching the
|
||||
// rest (name/description/key) straight from it.
|
||||
async function connectThisServer() {
|
||||
setConnecting(true);
|
||||
setConnectError("");
|
||||
try {
|
||||
const result = await api.addServerLink();
|
||||
window.location.href = result.link;
|
||||
} catch (err) {
|
||||
setConnectError(errorMessage(err));
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
}
|
||||
// Server identity (name/icon) is admin-editable per Server Settings ->
|
||||
// Server identity, and takes over the sidebar branding when set -- the
|
||||
// operator's own server should look like their server, not like the
|
||||
|
|
@ -256,6 +282,29 @@ export function Shell({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
{canManageServer && (
|
||||
<div className="sidebar-server-actions">
|
||||
<button
|
||||
className="btn ghost sidebar-server-action"
|
||||
type="button"
|
||||
title={"Connect this browser's client to this server"}
|
||||
disabled={connecting}
|
||||
onClick={() => void connectThisServer()}
|
||||
>
|
||||
<Zap size={15} /> {"Connect"}
|
||||
</button>
|
||||
<button
|
||||
className="btn ghost sidebar-server-action"
|
||||
type="button"
|
||||
title={"Share server (get an add-server link)"}
|
||||
onClick={() => setAddServerLinkOpen(true)}
|
||||
>
|
||||
<Share2 size={15} /> {"Share"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{connectError && <div className="sidebar-server-action-error">{connectError}</div>}
|
||||
{addServerLinkOpen && <AddServerLinkModal onClose={() => setAddServerLinkOpen(false)} />}
|
||||
</aside>
|
||||
<div className="workspace">
|
||||
<header className="topbar">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue