From 079ce23363c2bf8b10f34a6b18a0d34c40021558 Mon Sep 17 00:00:00 2001 From: Lionarius Date: Wed, 21 May 2025 08:46:12 +0300 Subject: [PATCH] . --- app/.prettierrc | 3 + app/app.css | 346 +++++++------- app/components/app-layout.tsx | 48 +- app/components/channel-area.tsx | 157 +++++-- app/components/chat-message-attachment.tsx | 50 +- app/components/chat-message.tsx | 122 +++-- .../custom-ui/channel-list-item.tsx | 203 ++++++--- .../custom-ui/create-server-button.tsx | 13 +- app/components/custom-ui/home-button.tsx | 27 +- app/components/custom-ui/online-status.tsx | 18 +- app/components/custom-ui/password-input.tsx | 19 +- .../custom-ui/private-channel-list-item.tsx | 95 ++-- app/components/custom-ui/server-button.tsx | 58 +-- app/components/custom-ui/settings-button.tsx | 36 +- app/components/custom-ui/text-box.tsx | 57 ++- app/components/custom-ui/user-status.tsx | 49 +- app/components/file-icon.tsx | 21 +- app/components/icons/Discord.tsx | 16 +- .../gateway-websocket-connection-manager.tsx | 24 +- .../manager/webrtc-connection-manager.tsx | 11 +- app/components/message-box.tsx | 277 +++++++---- .../modals/create-server-channel-modal.tsx | 109 +++-- .../modals/create-server-invite-modal.tsx | 74 ++- app/components/modals/create-server-modal.tsx | 78 +++- .../modals/delete-server-confirm-modal.tsx | 27 +- .../modals/update-profile-modal.tsx | 97 ++-- app/components/providers/modal-provider.tsx | 2 +- app/components/theme/theme-provider.tsx | 101 ++-- app/components/theme/theme-toggle.tsx | 73 +-- app/components/ui/aspect-ratio.tsx | 8 +- app/components/ui/avatar.tsx | 75 ++- app/components/ui/badge.tsx | 72 +-- app/components/ui/button.tsx | 99 ++-- app/components/ui/card.tsx | 131 +++--- app/components/ui/context-menu.tsx | 256 +++++++++++ app/components/ui/dialog.tsx | 185 ++++---- app/components/ui/dropdown-menu.tsx | 378 +++++++-------- app/components/ui/form.tsx | 245 +++++----- app/components/ui/icon-upload-field.tsx | 141 ++++-- app/components/ui/input.tsx | 32 +- app/components/ui/label.tsx | 34 +- app/components/ui/scroll-area.tsx | 138 +++--- app/components/ui/select.tsx | 284 ++++++------ app/components/ui/separator.tsx | 40 +- app/components/ui/tabs.tsx | 92 ++-- app/components/ui/textarea.tsx | 26 +- app/components/ui/tooltip.tsx | 82 ++-- app/components/user-avatar.tsx | 17 +- app/components/user-context-menu.tsx | 62 +++ app/components/visible-trigger.tsx | 12 +- app/hooks/use-origin.ts | 9 +- app/lib/api/client/auth.ts | 28 +- app/lib/api/client/channel.ts | 16 +- app/lib/api/client/file.ts | 22 +- app/lib/api/client/server.ts | 72 +-- app/lib/api/client/user.ts | 43 +- app/lib/api/http-client.ts | 24 +- app/lib/api/types.ts | 169 +++---- app/lib/consts.ts | 2 +- app/lib/utils.ts | 50 +- app/lib/websocket/gateway/client.ts | 115 +++-- app/lib/websocket/gateway/types.ts | 76 +-- app/lib/websocket/voice/client.ts | 110 +++-- app/lib/websocket/voice/types.ts | 40 +- app/root.tsx | 123 ++--- app/routes.ts | 22 +- app/routes/app/index.tsx | 2 +- app/routes/app/invite.tsx | 14 +- app/routes/app/layout.tsx | 46 +- app/routes/app/me/channel.tsx | 54 ++- app/routes/app/me/index.tsx | 1 - app/routes/app/me/layout.tsx | 39 +- app/routes/app/providers.tsx | 34 +- app/routes/app/server/channel.tsx | 19 +- app/routes/app/server/index.tsx | 1 - app/routes/app/server/layout.tsx | 143 +++--- app/routes/app/settings.tsx | 202 +++++--- app/routes/auth/login.tsx | 109 +++-- app/routes/auth/register.tsx | 94 +++- app/routes/index.tsx | 10 +- app/stores/channels-voice-state.tsx | 31 +- app/stores/gateway-store.ts | 200 +++++--- app/stores/modal-store.ts | 15 +- app/stores/private-channels-store.ts | 44 +- app/stores/server-channels-store.ts | 59 +-- app/stores/server-list-store.ts | 41 +- app/stores/token-store.ts | 16 +- app/stores/users-store.tsx | 122 +++-- app/stores/voice-state-store.ts | 10 +- app/stores/webrtc-store.ts | 20 +- bun.lock | 431 +++++++++++++++++- components.json | 2 +- eslint.config.js | 23 + package.json | 11 +- 94 files changed, 4630 insertions(+), 2704 deletions(-) create mode 100644 app/.prettierrc create mode 100644 app/components/ui/context-menu.tsx create mode 100644 app/components/user-context-menu.tsx create mode 100644 eslint.config.js diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 0000000..0a02bce --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/app/app.css b/app/app.css index 9a144fc..724d84f 100644 --- a/app/app.css +++ b/app/app.css @@ -4,194 +4,204 @@ @custom-variant dark (&:is(.dark *)); @theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-sans: + "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } :root { - --background: oklch(1.00 0 0); - --foreground: oklch(0.32 0 0); - --card: oklch(1.00 0 0); - --card-foreground: oklch(0.32 0 0); - --popover: oklch(1.00 0 0); - --popover-foreground: oklch(0.32 0 0); - --primary: oklch(0.62 0.19 259.81); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.97 0.00 264.54); - --secondary-foreground: oklch(0.45 0.03 256.80); - --muted: oklch(0.98 0.00 247.84); - --muted-foreground: oklch(0.55 0.02 264.36); - --accent: oklch(0.95 0.03 236.82); - --accent-foreground: oklch(0.38 0.14 265.52); - --destructive: oklch(0.64 0.21 25.33); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.93 0.01 264.53); - --input: oklch(0.93 0.01 264.53); - --ring: oklch(0.62 0.19 259.81); - --chart-1: oklch(0.62 0.19 259.81); - --chart-2: oklch(0.55 0.22 262.88); - --chart-3: oklch(0.49 0.22 264.38); - --chart-4: oklch(0.42 0.18 265.64); - --chart-5: oklch(0.38 0.14 265.52); - --sidebar: oklch(0.98 0.00 247.84); - --sidebar-foreground: oklch(0.32 0 0); - --sidebar-primary: oklch(0.62 0.19 259.81); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.95 0.03 236.82); - --sidebar-accent-foreground: oklch(0.38 0.14 265.52); - --sidebar-border: oklch(0.93 0.01 264.53); - --sidebar-ring: oklch(0.62 0.19 259.81); - --font-sans: Inter, sans-serif; - --font-serif: Source Serif 4, serif; - --font-mono: JetBrains Mono, monospace; - --radius: 0.375rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(1 0 0); + --foreground: oklch(0.32 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.32 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.32 0 0); + --primary: oklch(0.62 0.19 259.81); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.97 0 264.54); + --secondary-foreground: oklch(0.45 0.03 256.8); + --muted: oklch(0.98 0 247.84); + --muted-foreground: oklch(0.55 0.02 264.36); + --accent: oklch(0.95 0.03 236.82); + --accent-foreground: oklch(0.38 0.14 265.52); + --destructive: oklch(0.64 0.21 25.33); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.93 0.01 264.53); + --input: oklch(0.93 0.01 264.53); + --ring: oklch(0.62 0.19 259.81); + --chart-1: oklch(0.62 0.19 259.81); + --chart-2: oklch(0.55 0.22 262.88); + --chart-3: oklch(0.49 0.22 264.38); + --chart-4: oklch(0.42 0.18 265.64); + --chart-5: oklch(0.38 0.14 265.52); + --sidebar: oklch(0.98 0 247.84); + --sidebar-foreground: oklch(0.32 0 0); + --sidebar-primary: oklch(0.62 0.19 259.81); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.95 0.03 236.82); + --sidebar-accent-foreground: oklch(0.38 0.14 265.52); + --sidebar-border: oklch(0.93 0.01 264.53); + --sidebar-ring: oklch(0.62 0.19 259.81); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow-md: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1); + --shadow-lg: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1); + --shadow-xl: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); } .dark { - --background: oklch(0.20 0 0); - --foreground: oklch(0.92 0 0); - --card: oklch(0.27 0 0); - --card-foreground: oklch(0.92 0 0); - --popover: oklch(0.27 0 0); - --popover-foreground: oklch(0.92 0 0); - --primary: oklch(0.62 0.19 259.81); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.27 0 0); - --secondary-foreground: oklch(0.92 0 0); - --muted: oklch(0.27 0 0); - --muted-foreground: oklch(0.72 0 0); - --accent: oklch(0.38 0.14 265.52); - --accent-foreground: oklch(0.88 0.06 254.13); - --destructive: oklch(0.64 0.21 25.33); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.37 0 0); - --input: oklch(0.37 0 0); - --ring: oklch(0.62 0.19 259.81); - --chart-1: oklch(0.71 0.14 254.62); - --chart-2: oklch(0.62 0.19 259.81); - --chart-3: oklch(0.55 0.22 262.88); - --chart-4: oklch(0.49 0.22 264.38); - --chart-5: oklch(0.42 0.18 265.64); - --sidebar: oklch(0.20 0 0); - --sidebar-foreground: oklch(0.92 0 0); - --sidebar-primary: oklch(0.62 0.19 259.81); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.38 0.14 265.52); - --sidebar-accent-foreground: oklch(0.88 0.06 254.13); - --sidebar-border: oklch(0.37 0 0); - --sidebar-ring: oklch(0.62 0.19 259.81); - --font-sans: Inter, sans-serif; - --font-serif: Source Serif 4, serif; - --font-mono: JetBrains Mono, monospace; - --radius: 0.375rem; - --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); - --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); - --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); - --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); - --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); - --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); + --background: oklch(0.2 0 0); + --foreground: oklch(0.92 0 0); + --card: oklch(0.27 0 0); + --card-foreground: oklch(0.92 0 0); + --popover: oklch(0.27 0 0); + --popover-foreground: oklch(0.92 0 0); + --primary: oklch(0.62 0.19 259.81); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.27 0 0); + --secondary-foreground: oklch(0.92 0 0); + --muted: oklch(0.27 0 0); + --muted-foreground: oklch(0.72 0 0); + --accent: oklch(0.38 0.14 265.52); + --accent-foreground: oklch(0.88 0.06 254.13); + --destructive: oklch(0.64 0.21 25.33); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.37 0 0); + --input: oklch(0.37 0 0); + --ring: oklch(0.62 0.19 259.81); + --chart-1: oklch(0.71 0.14 254.62); + --chart-2: oklch(0.62 0.19 259.81); + --chart-3: oklch(0.55 0.22 262.88); + --chart-4: oklch(0.49 0.22 264.38); + --chart-5: oklch(0.42 0.18 265.64); + --sidebar: oklch(0.2 0 0); + --sidebar-foreground: oklch(0.92 0 0); + --sidebar-primary: oklch(0.62 0.19 259.81); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.38 0.14 265.52); + --sidebar-accent-foreground: oklch(0.88 0.06 254.13); + --sidebar-border: oklch(0.37 0 0); + --sidebar-ring: oklch(0.62 0.19 259.81); + --font-sans: Inter, sans-serif; + --font-serif: Source Serif 4, serif; + --font-mono: JetBrains Mono, monospace; + --radius: 0.375rem; + --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); + --shadow-sm: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1); + --shadow-md: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1); + --shadow-lg: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1); + --shadow-xl: + 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1); + --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); - --font-sans: var(--font-sans); - --font-mono: var(--font-mono); - --font-serif: var(--font-serif); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); - --shadow-2xs: var(--shadow-2xs); - --shadow-xs: var(--shadow-xs); - --shadow-sm: var(--shadow-sm); - --shadow: var(--shadow); - --shadow-md: var(--shadow-md); - --shadow-lg: var(--shadow-lg); - --shadow-xl: var(--shadow-xl); - --shadow-2xl: var(--shadow-2xl); + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); } @layer base { - /* width */ - ::-webkit-scrollbar { - @apply w-1 - } + /* width */ + ::-webkit-scrollbar { + @apply w-1; + } - /* Handle */ - ::-webkit-scrollbar-thumb { - @apply bg-border rounded-full mr-0.5 - } + /* Handle */ + ::-webkit-scrollbar-thumb { + @apply bg-border rounded-full mr-0.5; + } - * { - @apply border-border outline-ring/50; - } + * { + @apply border-border outline-ring/50; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } - button, - [role="button"] { - cursor: pointer; - } + button, + [role="button"] { + cursor: pointer; + } } @layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } - /* Hide scrollbar for Chrome, Safari and Opera */ - .no-scrollbar::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - .no-scrollbar { - /* IE and Edge */ - -ms-overflow-style: none; - /* Firefox */ - scrollbar-width: none; - } -} \ No newline at end of file + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + /* IE and Edge */ + -ms-overflow-style: none; + /* Firefox */ + scrollbar-width: none; + } +} diff --git a/app/components/app-layout.tsx b/app/components/app-layout.tsx index 5a49859..bc44d4b 100644 --- a/app/components/app-layout.tsx +++ b/app/components/app-layout.tsx @@ -14,31 +14,45 @@ interface AppLayoutProps { } export default function AppLayout({ children }: AppLayoutProps) { - let servers = useServerListStore(useShallow((state) => Object.values(state.servers))) + let servers = Object.values( + useServerListStore(useShallow((state) => state.servers)), + ); const matches = useMatches(); let list = React.useMemo(() => { - return matches.map(match => (match.handle as { - listComponent?: React.ReactNode - })?.listComponent).reverse().find(component => !!component) - }, [matches]) + return matches + .map( + (match) => + ( + match.handle as { + listComponent?: React.ReactNode; + } + )?.listComponent, + ) + .reverse() + .find((component) => !!component); + }, [matches]); return (
-
+
@@ -53,9 +67,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
-
- {children} -
+
{children}
- ) -} \ No newline at end of file + ); +} diff --git a/app/components/channel-area.tsx b/app/components/channel-area.tsx index b1437fc..a584906 100644 --- a/app/components/channel-area.tsx +++ b/app/components/channel-area.tsx @@ -1,23 +1,29 @@ -import { useInfiniteQuery, type QueryFunctionContext } from "@tanstack/react-query" -import type { Channel, MessageId } from "~/lib/api/types" -import ChatMessage from "./chat-message" -import MessageBox from "./message-box" -import VisibleTrigger from "./visible-trigger" +import { + useInfiniteQuery, + type QueryFunctionContext, +} from "@tanstack/react-query"; +import type { Channel, Message, MessageId } from "~/lib/api/types"; +import ChatMessage from "./chat-message"; +import MessageBox from "./message-box"; +import { Separator } from "./ui/separator"; +import VisibleTrigger from "./visible-trigger"; interface ChannelAreaProps { - channel: Channel + channel: Channel; } -export default function ChannelArea( - { channel }: ChannelAreaProps -) { - const channelId = channel.id +export default function ChannelArea({ channel }: ChannelAreaProps) { + const channelId = channel.id; const fetchMessages = async ({ pageParam }: QueryFunctionContext) => { - return await import("~/lib/api/client/channel").then(m => m.default.paginatedMessages(channelId, 50, pageParam as MessageId | undefined)) - } - - + return await import("~/lib/api/client/channel").then((m) => + m.default.paginatedMessages( + channelId, + 50, + pageParam as MessageId | undefined, + ), + ); + }; const { data, @@ -32,38 +38,38 @@ export default function ChannelArea( queryKey: ["messages", channelId], initialPageParam: undefined, queryFn: fetchMessages, - getNextPageParam: (lastPage) => lastPage.length < 50 ? undefined : lastPage[lastPage.length - 1]?.id, + getNextPageParam: (lastPage) => + lastPage.length < 50 + ? undefined + : lastPage[lastPage.length - 1]?.id, staleTime: Infinity, - }) + }); const fetchNextPageVisible = () => { - if (!isFetchingNextPage && hasNextPage) - fetchNextPage() - } + if (!isFetchingNextPage && hasNextPage) fetchNextPage(); + }; - let messageArea = null + let messageArea = null; if (isPending) { - messageArea =
- Loading... -
- } else { - messageArea = <> -
-
- { - status === "success" && data.pages.map((page, i) => ( - page.map((message) => ( -
- -
- )) - ) - ) - } - + messageArea = ( +
+ Loading...
- + ); + } else { + messageArea = ( + <> +
+
+ {status === "success" && renderMessages(data.pages)} + +
+ + ); } return ( @@ -75,10 +81,79 @@ export default function ChannelArea(
{messageArea}
-
+
); -} \ No newline at end of file +} + +function renderMessages(pages: Message[][]) { + const messageElements: React.ReactNode[] = []; + let lastDate: string | null = null; + + const formatMessageDate = (date: Date) => { + const now = new Date(); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const messageDate = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + ); + + const capitalize = (str: string) => + str.charAt(0).toUpperCase() + str.slice(1); + + if (messageDate.getTime() === today.getTime()) { + const rtf = new Intl.RelativeTimeFormat(undefined, { + numeric: "auto", + }); + return capitalize(rtf.format(0, "day")); + } else if (messageDate.getTime() === yesterday.getTime()) { + const rtf = new Intl.RelativeTimeFormat(undefined, { + numeric: "auto", + }); + return capitalize(rtf.format(-1, "day")); + } else { + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }); + } + }; + + pages.forEach((page) => { + page.forEach((message) => { + const messageDate = message.createdAt.toDateString(); + if (messageDate != lastDate) { + if (lastDate) + messageElements.push( +
+ + + {formatMessageDate(new Date(lastDate))} + + +
, + ); + lastDate = messageDate; + } + + messageElements.push( +
+ +
, + ); + }); + }); + + return messageElements; +} diff --git a/app/components/chat-message-attachment.tsx b/app/components/chat-message-attachment.tsx index b9279ee..3896abe 100644 --- a/app/components/chat-message-attachment.tsx +++ b/app/components/chat-message-attachment.tsx @@ -1,8 +1,4 @@ -import { - Download, - ExternalLink, - Maximize -} from "lucide-react"; +import { Download, ExternalLink, Maximize } from "lucide-react"; import { AspectRatio } from "~/components/ui/aspect-ratio"; // Shadcn UI AspectRatio import { Button } from "~/components/ui/button"; // Shadcn UI Button import { @@ -28,7 +24,9 @@ interface ChatMessageAttachmentProps { file: UploadedFile; } -export default function ChatMessageAttachment({ file }: ChatMessageAttachmentProps) { +export default function ChatMessageAttachment({ + file, +}: ChatMessageAttachmentProps) { if (file.contentType.startsWith("image/")) { return ; } @@ -38,9 +36,12 @@ export default function ChatMessageAttachment({ file }: ChatMessageAttachmentPro function GenericFileAttachment({ file }: ChatMessageAttachmentProps) { return ( -
-
- +
+
+

@@ -54,7 +55,12 @@ function GenericFileAttachment({ file }: ChatMessageAttachmentProps) { @@ -85,7 +97,10 @@ function ImageAttachment({ file }: ChatMessageAttachmentProps) {

- + {file.filename} - {file.filename} + + {file.filename} +
- {channelUsers.length > 0 && + + + + + + onDeleteChannel(channel)} + > + Delete + + + + {channelUsers.length > 0 && (
- { - channelUsers - .map(user => ( -
- - {user.displayName || user.username} -
- )) - } -
} + {channelUsers.map((user) => ( +
+ + {user.displayName || user.username} +
+ ))} +
+ )} - ) + ); } function ServerText({ channel }: ChannelListItemProps) { return ( - + {({ isActive }) => ( - - ) - } + + + + + + onDeleteChannel(channel)} + > + Delete + + + + )} - ) + ); } -export default function ServerChannelListItem({ channel }: ChannelListItemProps) { +export default function ServerChannelListItem({ + channel, +}: ChannelListItemProps) { switch (channel.type) { case ChannelType.SERVER_CATEGORY: - return + return ; case ChannelType.SERVER_VOICE: - return + return ; case ChannelType.SERVER_TEXT: - return + return ; default: - return null + return null; } } diff --git a/app/components/custom-ui/create-server-button.tsx b/app/components/custom-ui/create-server-button.tsx index 7dba44c..c77d156 100644 --- a/app/components/custom-ui/create-server-button.tsx +++ b/app/components/custom-ui/create-server-button.tsx @@ -1,15 +1,18 @@ import { CirclePlus } from "lucide-react"; - import { Button } from "~/components/ui/button"; import { ModalType, useModalStore } from "~/stores/modal-store"; export function CreateServerButton() { - const onOpen = useModalStore(state => state.onOpen) + const onOpen = useModalStore((state) => state.onOpen); return ( - - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/home-button.tsx b/app/components/custom-ui/home-button.tsx index bec5cc0..4725a38 100644 --- a/app/components/custom-ui/home-button.tsx +++ b/app/components/custom-ui/home-button.tsx @@ -5,17 +5,18 @@ import { Button } from "../ui/button"; export function HomeButton() { return ( - { - ({ isActive }) => ( - - ) - } + {({ isActive }) => ( + + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/online-status.tsx b/app/components/custom-ui/online-status.tsx index d2d9040..1539bc2 100644 --- a/app/components/custom-ui/online-status.tsx +++ b/app/components/custom-ui/online-status.tsx @@ -10,11 +10,19 @@ export function OnlineStatus({
- {status === "online" && } - {status === "dnd" && } - {status === "idle" && } - {status === "offline" && } + {status === "online" && ( + + )} + {status === "dnd" && ( + + )} + {status === "idle" && ( + + )} + {status === "offline" && ( + + )}
); -} \ No newline at end of file +} diff --git a/app/components/custom-ui/password-input.tsx b/app/components/custom-ui/password-input.tsx index b4c6235..e05511a 100644 --- a/app/components/custom-ui/password-input.tsx +++ b/app/components/custom-ui/password-input.tsx @@ -1,11 +1,12 @@ -import { EyeIcon, EyeOffIcon } from "lucide-react" -import React from "react" -import { Button } from "../ui/button" -import { Input } from "../ui/input" +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import React from "react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; export function PasswordInput(props: React.ComponentProps<"input">) { - const [showPassword, setShowPassword] = React.useState(false) - const disabled = props.value === '' || props.value === undefined || props.disabled + const [showPassword, setShowPassword] = React.useState(false); + const disabled = + props.value === "" || props.value === undefined || props.disabled; return (
@@ -23,7 +24,9 @@ export function PasswordInput(props: React.ComponentProps<"input">) { ) : (
- ) + ); } diff --git a/app/components/custom-ui/private-channel-list-item.tsx b/app/components/custom-ui/private-channel-list-item.tsx index b4e60e3..43c5fcd 100644 --- a/app/components/custom-ui/private-channel-list-item.tsx +++ b/app/components/custom-ui/private-channel-list-item.tsx @@ -1,48 +1,71 @@ -import { Check } from "lucide-react" -import { NavLink } from "react-router" -import type { RecipientChannel } from "~/lib/api/types" -import { cn } from "~/lib/utils" -import { useUsersStore } from "~/stores/users-store" -import { Badge } from "../ui/badge" -import { Button } from "../ui/button" -import UserAvatar from "../user-avatar" -import { OnlineStatus } from "./online-status" +import { Check } from "lucide-react"; +import { NavLink } from "react-router"; +import type { RecipientChannel } from "~/lib/api/types"; +import { cn } from "~/lib/utils"; +import { useUsersStore } from "~/stores/users-store"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import UserAvatar from "../user-avatar"; +import { OnlineStatus } from "./online-status"; interface PrivateChannelListItemProps { - channel: RecipientChannel + channel: RecipientChannel; } -export default function PrivateChannelListItem({ channel }: PrivateChannelListItemProps) { - const currentUserId = useUsersStore(state => state.currentUserId) - const recipients = channel.recipients.filter(recipient => recipient.id !== currentUserId); - const renderSystemBadge = recipients.some(recipient => recipient.system) && recipients.length === 1 +export default function PrivateChannelListItem({ + channel, +}: PrivateChannelListItemProps) { + const currentUserId = useUsersStore((state) => state.currentUserId); + const recipients = channel.recipients.filter( + (recipient) => recipient.id !== currentUserId, + ); + const renderSystemBadge = + recipients.some((recipient) => recipient.system) && + recipients.length === 1; return ( <> - { - ({ isActive }) => ( - - ) - } +
+ {recipients + .map( + (recipient) => + recipient.displayName || + recipient.username, + ) + .join(", ")} +
+ {renderSystemBadge && ( + + {" "} + System + + )} +
+ + )} - ) + ); } diff --git a/app/components/custom-ui/server-button.tsx b/app/components/custom-ui/server-button.tsx index e081d70..30d08f6 100644 --- a/app/components/custom-ui/server-button.tsx +++ b/app/components/custom-ui/server-button.tsx @@ -1,36 +1,36 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar" -import { NavLink } from "react-router" -import type { Server } from "~/lib/api/types" -import { getFirstLetters } from "~/lib/utils" -import { Button } from "../ui/button" +import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; +import { NavLink } from "react-router"; +import type { Server } from "~/lib/api/types"; +import { getFirstLetters } from "~/lib/utils"; +import { Button } from "../ui/button"; export interface ServerButtonProps { - server: Server + server: Server; } -export function ServerButton( - { server }: ServerButtonProps -) { +export function ServerButton({ server }: ServerButtonProps) { return ( - { - ({ isActive }) => ( - - ) - } + {({ isActive }) => ( + + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/settings-button.tsx b/app/components/custom-ui/settings-button.tsx index 5fdb142..019b1db 100644 --- a/app/components/custom-ui/settings-button.tsx +++ b/app/components/custom-ui/settings-button.tsx @@ -1,21 +1,33 @@ import { Settings } from "lucide-react"; +import { useNavigate } from "react-router"; import { ModalType, useModalStore } from "~/stores/modal-store"; import { useTokenStore } from "~/stores/token-store"; import { Button } from "../ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; export function SettingsButton() { - const setToken = useTokenStore(state => state.setToken) - const onOpen = useModalStore(state => state.onOpen) + const setToken = useTokenStore((state) => state.setToken); + const onOpen = useModalStore((state) => state.onOpen); + const navigate = useNavigate(); const onUpdateProfile = () => { - onOpen(ModalType.UPDATE_PROFILE) - } + onOpen(ModalType.UPDATE_PROFILE); + }; + + const onOpenSettings = () => { + navigate("/app/settings"); + }; const onLogout = () => { - setToken(undefined) - window.location.reload() - } + setToken(undefined); + window.location.reload(); + }; return ( @@ -25,8 +37,8 @@ export function SettingsButton() { - - Update profile + + Settings @@ -34,5 +46,5 @@ export function SettingsButton() { - ) -} \ No newline at end of file + ); +} diff --git a/app/components/custom-ui/text-box.tsx b/app/components/custom-ui/text-box.tsx index c8115fa..af4f9c0 100644 --- a/app/components/custom-ui/text-box.tsx +++ b/app/components/custom-ui/text-box.tsx @@ -1,7 +1,13 @@ -import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; -import { cn } from '~/lib/utils'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef, +} from "react"; +import { cn } from "~/lib/utils"; -export interface TextBoxProps extends Omit, 'onChange' | 'value'> { +export interface TextBoxProps + extends Omit, "onChange" | "value"> { value: string; onChange: (value: string) => void; placeholder?: string; @@ -28,7 +34,7 @@ export const TextBox = forwardRef( onFocus, ...rest }, - ref + ref, ) => { const localRef = useRef(null); useImperativeHandle(ref, () => localRef.current as HTMLDivElement); @@ -39,10 +45,13 @@ export const TextBox = forwardRef( // Only update if different to avoid selection issues if (localRef.current.textContent !== newValue) { localRef.current.textContent = newValue; - + // Clear any
elements if the content is empty - if (!newValue && localRef.current.innerHTML.includes('
')) { - localRef.current.innerHTML = ''; + if ( + !newValue && + localRef.current.innerHTML.includes("
") + ) { + localRef.current.innerHTML = ""; } } } @@ -60,34 +69,34 @@ export const TextBox = forwardRef( }, [autoFocus]); const handleInput = (event: React.FormEvent) => { - const newValue = event.currentTarget.textContent || ''; - + const newValue = event.currentTarget.textContent || ""; + // Handle the case where the content is empty but contains a
- if (!newValue && event.currentTarget.innerHTML.includes('
')) { - event.currentTarget.innerHTML = ''; + if (!newValue && event.currentTarget.innerHTML.includes("
")) { + event.currentTarget.innerHTML = ""; } - + onChange(newValue); onInput?.(event); }; const handlePaste = (event: React.ClipboardEvent) => { event.preventDefault(); - const text = event.clipboardData.getData('text/plain'); - + const text = event.clipboardData.getData("text/plain"); + // Use document.execCommand to maintain undo stack - document.execCommand('insertText', false, text); - + document.execCommand("insertText", false, text); + // Manually trigger input event - const inputEvent = new Event('input', { bubbles: true }); + const inputEvent = new Event("input", { bubbles: true }); event.currentTarget.dispatchEvent(inputEvent); }; return (
localRef.current?.focus()} > @@ -99,10 +108,10 @@ export const TextBox = forwardRef( onBlur={onBlur} onFocus={onFocus} className={cn( - "break-words whitespace-pre-wrap outline-none w-full", + "outline-none break-all", "empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:cursor-text", disabled && "cursor-not-allowed opacity-50", - inputClassName + inputClassName, )} data-placeholder={placeholder} role="textbox" @@ -116,9 +125,9 @@ export const TextBox = forwardRef( />
); - } + }, ); -TextBox.displayName = 'TextBox'; +TextBox.displayName = "TextBox"; -export default TextBox; \ No newline at end of file +export default TextBox; diff --git a/app/components/custom-ui/user-status.tsx b/app/components/custom-ui/user-status.tsx index d3ad5ee..ca6c456 100644 --- a/app/components/custom-ui/user-status.tsx +++ b/app/components/custom-ui/user-status.tsx @@ -11,16 +11,22 @@ import { OnlineStatus } from "./online-status"; import { SettingsButton } from "./settings-button"; function VoiceStatus({ - voiceState -}: { voiceState: { serverId: string; channelId: string } }) { + voiceState, +}: { + voiceState: { serverId: string; channelId: string }; +}) { // const webrtcState = useWebRTCStore(state => state.status) const leaveVoiceChannel = () => { - useVoiceStateStore.getState().leaveVoiceChannel() - } + useVoiceStateStore.getState().leaveVoiceChannel(); + }; - const channel = useServerChannelsStore(state => state.channels[voiceState.serverId]?.[voiceState.channelId]) - const server = useServerListStore(state => state.servers[voiceState.serverId]) + const channel = useServerChannelsStore( + (state) => state.channels[voiceState.serverId]?.[voiceState.channelId], + ); + const server = useServerListStore( + (state) => state.servers[voiceState.serverId], + ); return (
@@ -35,20 +41,21 @@ function VoiceStatus({
- ) + ); } export default function UserStatus() { - const user = useUsersStore(state => state.getCurrentUser()!) - const voiceState = useVoiceStateStore(state => state.activeChannel) + const user = useUsersStore((state) => state.getCurrentUser()!); + const voiceState = useVoiceStateStore((state) => state.activeChannel); return ( -
- {voiceState && <> - - - - } +
+ {voiceState && ( + <> + + + + )}
@@ -57,9 +64,13 @@ export default function UserStatus() {
- {user?.displayName || user?.username || "Unknown user"} + {user?.displayName || + user?.username || + "Unknown user"}
- @{user?.username} + + @{user?.username} +
@@ -76,5 +87,5 @@ export default function UserStatus() {
- ) -} \ No newline at end of file + ); +} diff --git a/app/components/file-icon.tsx b/app/components/file-icon.tsx index 9b7d522..317303a 100644 --- a/app/components/file-icon.tsx +++ b/app/components/file-icon.tsx @@ -1,10 +1,11 @@ import { FileArchive, FileAudio, + FileImage, FileQuestion, + FileSpreadsheet, FileText, FileVideo, - ImageIcon, type LucideProps, } from "lucide-react"; @@ -17,7 +18,7 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { const commonProps = { className: className ?? "h-5 w-5", ...props }; if (contentType.startsWith("image/")) { - return ; + return ; } if (contentType.startsWith("audio/")) { return ; @@ -30,19 +31,25 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { } if ( contentType.startsWith("application/vnd.ms-excel") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.spreadsheetml", + ) ) { - return ; // Could use a specific Excel icon if available/desired + return ; // Could use a specific Excel icon if available/desired } if ( contentType.startsWith("application/msword") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.wordprocessingml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.wordprocessingml", + ) ) { return ; } if ( contentType.startsWith("application/vnd.ms-powerpoint") || - contentType.startsWith("application/vnd.openxmlformats-officedocument.presentationml") + contentType.startsWith( + "application/vnd.openxmlformats-officedocument.presentationml", + ) ) { return ; } @@ -59,4 +66,4 @@ export function FileIcon({ contentType, className, ...props }: FileIconProps) { return ; } return ; // Default for unknown types -} \ No newline at end of file +} diff --git a/app/components/icons/Discord.tsx b/app/components/icons/Discord.tsx index 618b300..a85da0a 100644 --- a/app/components/icons/Discord.tsx +++ b/app/components/icons/Discord.tsx @@ -1,5 +1,19 @@ import type { SVGProps } from "react"; -const Discord = (props: SVGProps) => ; +const Discord = (props: SVGProps) => ( + + + +); export default Discord; diff --git a/app/components/manager/gateway-websocket-connection-manager.tsx b/app/components/manager/gateway-websocket-connection-manager.tsx index 8ed9860..917df63 100644 --- a/app/components/manager/gateway-websocket-connection-manager.tsx +++ b/app/components/manager/gateway-websocket-connection-manager.tsx @@ -1,20 +1,18 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import { ConnectionState } from '~/lib/websocket/gateway/types'; -import { useGatewayStore } from '~/stores/gateway-store'; -import { useTokenStore } from '~/stores/token-store'; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; +import { ConnectionState } from "~/lib/websocket/gateway/types"; +import { useGatewayStore } from "~/stores/gateway-store"; +import { useTokenStore } from "~/stores/token-store"; export function GatewayWebSocketConnectionManager() { - const token = useTokenStore((state) => - state.token, - ); + const token = useTokenStore((state) => state.token); const { setQueryClient } = useGatewayStore(); const queryClient = useQueryClient(); useEffect(() => { setQueryClient(queryClient); - }, [queryClient]) + }, [queryClient]); useEffect(() => { const { status, connect, disconnect } = useGatewayStore.getState(); @@ -34,9 +32,5 @@ export function GatewayWebSocketConnectionManager() { }; }, [token]); - return ( - <> - {null} - - ); -} \ No newline at end of file + return <>{null}; +} diff --git a/app/components/manager/webrtc-connection-manager.tsx b/app/components/manager/webrtc-connection-manager.tsx index 489275d..ebd607d 100644 --- a/app/components/manager/webrtc-connection-manager.tsx +++ b/app/components/manager/webrtc-connection-manager.tsx @@ -9,11 +9,11 @@ export function WebRTCConnectionManager() { const voiceState = useVoiceStateStore(); const webrtc = useWebRTCStore(); - const remoteStream = useWebRTCStore(state => state.remoteStream); - const audioRef = useRef(null) + const remoteStream = useWebRTCStore((state) => state.remoteStream); + const audioRef = useRef(null); if (audioRef.current) { - audioRef.current.srcObject = remoteStream + audioRef.current.srcObject = remoteStream; } useEffect(() => { @@ -25,7 +25,7 @@ export function WebRTCConnectionManager() { audio: { noiseSuppression: false, }, - video: false + video: false, }); webrtc.createOffer(stream); @@ -41,7 +41,6 @@ export function WebRTCConnectionManager() { if (webrtc.status === ConnectionState.DISCONNECTED) { voiceState.leaveVoiceChannel(); } - }, [webrtc.status]); return ( @@ -49,4 +48,4 @@ export function WebRTCConnectionManager() {