(feat): add profile page
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m51s
/ build-mobile (push) Successful in 16m33s
/ release (push) Successful in 23s

(feat): show user online-status
(feat): make markdown input more modular
This commit is contained in:
Alois 2026-05-16 14:41:43 +02:00
commit 55da8026a7
12 changed files with 609 additions and 63 deletions

View file

@ -161,7 +161,8 @@ export default function View() {
const focusedParticipantHasActiveScreenShare =
activeScreenShareParticipantIdSet.has(focusedParticipantId);
const focusedTileType: "user" | "stream" =
focusedParticipantType === "stream" && focusedParticipantHasActiveScreenShare
focusedParticipantType === "stream" &&
focusedParticipantHasActiveScreenShare
? "stream"
: "user";
const isImmersiveFocusedView = callIsFullscreen && usersInFocusedViewHidden;

View file

@ -97,6 +97,8 @@ export default function InputComponent({
>
<CardHeader className="relative p-0 flex flex-col">
<Input
paddingY="13px"
paddingX="13px"
placeholder="Send a message..."
value={value}
setValue={setValue}

View file

@ -23,6 +23,7 @@ import {
indentWithTab,
} from "@codemirror/commands";
import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import { collectInlineRanges, ensureMarkdownStyles } from "./markdown";
@ -33,8 +34,36 @@ export type InputProps = {
setValue: (value: string) => void;
onSubmit?: () => void;
invertEnterBehavior?: boolean;
styled?: boolean;
fontSize?: CSSProperties["fontSize"];
paddingX?: CSSProperties["padding"];
paddingY?: CSSProperties["padding"];
className?: string;
};
type InputStyle = CSSProperties & {
"--tm-md-content-padding"?: string;
};
function toCssLength(value: CSSProperties["padding"]): string | undefined {
if (value === undefined) {
return undefined;
}
return typeof value === "number" ? `${value}px` : value;
}
function toCssPadding(
vertical: CSSProperties["padding"],
horizontal: CSSProperties["padding"],
styled: boolean,
): string {
const defaultVertical = styled ? "0.25rem" : "0";
const defaultHorizontal = styled ? "0.625rem" : "0";
return `${toCssLength(vertical) ?? defaultVertical} ${toCssLength(horizontal) ?? defaultHorizontal}`;
}
type TokenRange = {
from: number;
to: number;
@ -80,6 +109,10 @@ const markdownDecorations = ViewPlugin.fromClass(
export default function Input(props: InputProps) {
ensureMarkdownStyles();
const shellClassName = props.styled
? "min-h-8 w-full min-w-0 rounded-lg border border-input bg-transparent text-base transition-colors outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
: "";
const elementRef = useRef<HTMLDivElement | null>(null);
const viewRef = useRef<EditorView | undefined>(undefined);
const ignoreSyncRef = useRef(false);
@ -143,7 +176,22 @@ export default function Input(props: InputProps) {
});
}, [props.value]);
return <div ref={elementRef} className="tm-md-root w-full" />;
return (
<div
ref={elementRef}
className={`tm-md-root ${shellClassName} ${props.className ?? ""}`}
style={
{
fontSize: props.fontSize ?? "1rem",
"--tm-md-content-padding": toCssPadding(
props.paddingY,
props.paddingX,
Boolean(props.styled),
),
} as InputStyle
}
/>
);
}
/**
@ -204,7 +252,7 @@ function createEditorExtensions(
}),
EditorView.theme({
"&": {
fontSize: "1rem",
fontSize: "inherit",
},
"&.cm-editor": {
width: "100%",

View file

@ -665,12 +665,12 @@ export const markdownStyles = `
.tm-md-table th { background: hsl(var(--muted)); font-weight: 600; }
.tm-md-hr { margin: 0.55rem 0; }
.cm-editor.tm-md-editor { border-radius: 0.65rem; background: hsl(var(--card)); caret-color: var(--foreground); }
.cm-editor.tm-md-editor { border-radius: inherit; background: transparent; caret-color: var(--foreground); }
.cm-editor.tm-md-editor.cm-focused { outline: none; box-shadow: none; }
.cm-editor.tm-md-editor .cm-scroller { font-family: inherit; line-height: 1.55; max-height: 30vh; overflow-y: auto; overflow-x: hidden; }
.cm-editor.tm-md-editor .cm-content { caret-color: var(--foreground); }
.cm-editor.tm-md-editor .cm-content { padding: 0.7rem 0.85rem; min-height: 2.75rem; }
.cm-editor.tm-md-editor .cm-line { padding: 0 1px; color: hsl(var(--foreground)); }
.cm-editor.tm-md-editor .cm-content { padding: var(--tm-md-content-padding, 0.25rem 0.625rem); min-height: 2rem; }
.cm-editor.tm-md-editor .cm-line { padding: 0; color: hsl(var(--foreground)); }
.cm-editor.tm-md-editor .tm-md-hidden-token { color: transparent; opacity: 0; font-size: inherit; }
.cm-editor.tm-md-editor .tm-md-code-line { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; background: hsl(var(--muted)); border-radius: 0.3rem; }
`;

View file

@ -48,6 +48,31 @@ export type Communities = z.infer<
export type Calls = z.infer<typeof ttp.challenge_response.response.shape.calls>;
// TTP
const user = z.object({
about: z.string().max(255).optional(),
avatar: z.string().optional(),
display: z.string().min(1).max(15),
iota_id: z.number(),
omikron_connections: z.array(z.number()),
omikron_id: z.number().optional(),
online_status: z.enum([
"user_offline",
"user_online",
"user_dnd",
"user_idle",
"user_wc",
"user_borked",
"iota_offline",
"iota_online",
"iota_borked",
]),
public_key: z.base64(),
status: z.string().max(15).optional(),
sub_end: z.number(),
sub_level: z.number(),
user_id: z.number(),
username: z.string().min(1).max(15),
});
export const ttp = {
identification: {
request: z.object({
@ -92,31 +117,11 @@ export const ttp = {
request: z.object({
user_id: z.number(),
}),
response: z.object({
about: z.string().max(255).optional(),
avatar: z.string().optional(),
display: z.string().max(15),
iota_id: z.number(),
omikron_connections: z.array(z.number()),
omikron_id: z.number().optional(),
online_status: z.enum([
"user_offline",
"user_online",
"user_dnd",
"user_idle",
"user_wc",
"user_borked",
"iota_offline",
"iota_online",
"iota_borked",
]),
public_key: z.base64(),
status: z.string().max(15).optional(),
sub_end: z.number(),
sub_level: z.number(),
user_id: z.number(),
username: z.string().max(15),
}),
response: user,
},
change_user_data: {
request: user.partial(),
response: z.object({}),
},
ping: {
request: z.object({
@ -283,3 +288,28 @@ export const storageDefaults: Storage = {
call_mute_range_start: -55,
call_mute_range_end: -45,
};
// User Status
export function getStatusColor(
status: z.infer<typeof ttp.get_user_data.response.shape.online_status>,
) {
switch (status) {
case "user_online":
return "#22c55e";
case "iota_online":
return "#22c55e";
case "user_dnd":
return "#ef4444";
case "user_idle":
return "#f59e0b";
case "user_wc":
return "#3b82f6";
case "user_borked":
case "iota_borked":
return "#6b7280";
case "user_offline":
case "iota_offline":
default:
return "#9ca3af";
}
}

View file

@ -51,7 +51,7 @@ export default function UserProvider(props: { children: React.ReactNode }) {
const user = {
...userData.data,
avatar: userData.data.avatar
? `data:image/png;base64,${userData.data.avatar}`
? `data:image/webp;base64,${atob(userData.data.avatar)}`
: undefined,
};