feat: add SMTP email support and fix profile/permissions bugs
This commit is contained in:
@@ -82,7 +82,7 @@ export function CreateChannelModal({ serverId, onClose }: CreateChannelModalProp
|
||||
const result = await api.post<ChannelApiResponse>('/servers/' + serverId + '/channels', body);
|
||||
const newChannel = {
|
||||
id: result.id,
|
||||
serverId: result.server_id,
|
||||
server_id: result.server_id,
|
||||
name: result.name,
|
||||
type: result.type,
|
||||
category: result.category || null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore, type UserStatus } from '../stores/auth.ts';
|
||||
import { useWebSocketStore } from '../stores/ws.ts';
|
||||
import { useServerStore } from '../stores/server.ts';
|
||||
import { useLayoutStore } from '../stores/layout.ts';
|
||||
import { ServerBar } from './ServerBar.tsx';
|
||||
import { ChannelList } from './ChannelList.tsx';
|
||||
import { ConversationList } from './ConversationList.tsx';
|
||||
@@ -29,7 +30,6 @@ export function Layout() {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const isLoading = useAuthStore((state) => state.isLoading);
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const fetchMe = useAuthStore((state) => state.fetchMe);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
const updateProfile = useAuthStore((state) => state.updateProfile);
|
||||
const wsConnect = useWebSocketStore((s) => s.connect);
|
||||
@@ -38,14 +38,14 @@ export function Layout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [showStatusMenu, setShowStatusMenu] = useState(false);
|
||||
const [dmMode, setDmMode] = useState(false);
|
||||
const isDM = useLayoutStore((s) => s.isDM);
|
||||
const setDM = useLayoutStore((s) => s.setDM);
|
||||
const [showServerSettings, setShowServerSettings] = useState(false);
|
||||
const activeServerId = useServerStore((s) => s.activeServerId);
|
||||
useEffect(() => {
|
||||
fetchMe();
|
||||
wsConnect();
|
||||
return () => { wsDisconnect(); };
|
||||
}, [fetchMe, wsConnect, wsDisconnect]);
|
||||
}, [wsConnect, wsDisconnect]);
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated && location.pathname !== '/login') {
|
||||
navigate('/login', { replace: true });
|
||||
@@ -120,9 +120,9 @@ export function Layout() {
|
||||
<div className="flex-1 flex min-h-0">
|
||||
<div className="flex flex-col items-center w-16 bg-gb-bg-h border-r border-gb-bg-t py-2 gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => setDmMode(false)}
|
||||
onClick={() => setDM(false)}
|
||||
className={`w-11 h-11 flex items-center justify-center font-mono text-xs border rounded ${
|
||||
!dmMode
|
||||
!isDM
|
||||
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
||||
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t'
|
||||
}`}
|
||||
@@ -132,11 +132,11 @@ export function Layout() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDmMode(true);
|
||||
setDM(true);
|
||||
setActiveServer(null);
|
||||
}}
|
||||
className={`w-11 h-11 flex items-center justify-center font-mono text-xs border rounded ${
|
||||
dmMode
|
||||
isDM
|
||||
? 'bg-gb-bg-t text-gb-orange border-gb-orange'
|
||||
: 'bg-gb-bg-s text-gb-fg border-gb-bg-t hover:border-gb-fg-t'
|
||||
}`}
|
||||
@@ -146,7 +146,7 @@ export function Layout() {
|
||||
</button>
|
||||
</div>
|
||||
<ServerBar />
|
||||
{dmMode ? <ConversationList /> : <ChannelList />}
|
||||
{isDM ? <ConversationList /> : <ChannelList />}
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||
<VoicePanel />
|
||||
@@ -155,7 +155,7 @@ export function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!dmMode && <MemberList />}
|
||||
{!isDM && <MemberList />}
|
||||
</div>
|
||||
{showServerSettings && activeServerId && (
|
||||
<ServerSettingsModal serverId={activeServerId} onClose={() => setShowServerSettings(false)} />
|
||||
|
||||
@@ -6,6 +6,8 @@ import { usePresenceStore } from "../stores/presence.ts";
|
||||
import type { UserStatus } from "../stores/auth.ts";
|
||||
import { MemberContextMenu } from "./MemberContextMenu.tsx";
|
||||
import { useConversationStore } from "../stores/conversation.ts";
|
||||
import { useLayoutStore } from "../stores/layout.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function statusIcon(status: UserStatus): string {
|
||||
switch (status) {
|
||||
@@ -53,7 +55,10 @@ function MemberRow({ member }: { member: Member }) {
|
||||
const currentUser = useAuthStore((s) => s.user);
|
||||
const userPerms = useServerStore((s) => s.userPermissions);
|
||||
const activeServerId = useServerStore((s) => s.activeServerId);
|
||||
const setActiveServer = useServerStore((s) => s.setActiveServer);
|
||||
const createConversation = useConversationStore((s) => s.createConversation);
|
||||
const setDM = useLayoutStore((s) => s.setDM);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const canKick = userPerms?.kick_members ?? false;
|
||||
const canBan = userPerms?.ban_members ?? false;
|
||||
@@ -79,9 +84,16 @@ function MemberRow({ member }: { member: Member }) {
|
||||
canKick={canKick}
|
||||
canBan={canBan}
|
||||
canMute={canMute}
|
||||
onMessage={() => {
|
||||
createConversation([member.id]);
|
||||
setMenuOpen(false);
|
||||
onMessage={async () => {
|
||||
try {
|
||||
const conv = await createConversation([member.id]);
|
||||
setMenuOpen(false);
|
||||
setActiveServer(null);
|
||||
setDM(true);
|
||||
navigate(`/dm/${conv.id}`);
|
||||
} catch (err) {
|
||||
console.error("Failed to start DM:", err);
|
||||
}
|
||||
}}
|
||||
onKick={(_reason) => {
|
||||
fetch(`/api/v1/servers/${activeServerId}/members/${member.id}`, { method: "DELETE", credentials: "include" })
|
||||
|
||||
@@ -15,7 +15,19 @@ export function NewConversationModal({ onClose }: NewConversationModalProps) {
|
||||
const activeServerId = useServerStore((s) => s.activeServerId);
|
||||
const membersByServer = useMemberStore((s) => s.membersByServer);
|
||||
const currentUserId = useAuthStore((s) => s.user?.id);
|
||||
const members = activeServerId ? membersByServer[activeServerId] || [] : [];
|
||||
|
||||
const members = useMemo(() => {
|
||||
const map = new Map<string, any>();
|
||||
if (activeServerId && membersByServer[activeServerId]) {
|
||||
membersByServer[activeServerId].forEach((m) => map.set(m.id, m));
|
||||
} else {
|
||||
// Global search if no active server
|
||||
Object.values(membersByServer).forEach((list) => {
|
||||
list.forEach((m) => map.set(m.id, m));
|
||||
});
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}, [activeServerId, membersByServer]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.toLowerCase();
|
||||
|
||||
@@ -322,20 +322,40 @@ export function UserSettings() {
|
||||
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
|
||||
NOTIFICATIONS:
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={isSubscribed ? unsubscribe : subscribe}
|
||||
className={`terminal-button text-xs ${isSubscribed ? 'border-gb-green text-gb-green' : 'border-gb-orange text-gb-orange'}`}
|
||||
>
|
||||
{isSubscribed ? '[DISABLE PUSH]' : '[ENABLE PUSH]'}
|
||||
</button>
|
||||
<span className="text-gb-fg-f text-xs font-mono">
|
||||
{isSubscribed ? '● subscribed' : '○ not subscribed'}
|
||||
</span>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (Notification.permission !== 'granted') {
|
||||
await Notification.requestPermission();
|
||||
// Force a re-render to update the permission status
|
||||
setDisplayName(displayName + ' '); setTimeout(() => setDisplayName(displayName), 0);
|
||||
}
|
||||
}}
|
||||
className={`terminal-button text-xs ${Notification.permission === 'granted' ? 'border-gb-green text-gb-green' : 'border-gb-orange text-gb-orange'}`}
|
||||
>
|
||||
{Notification.permission === 'granted' ? '[DESKTOP GRANTED]' : '[ENABLE DESKTOP]'}
|
||||
</button>
|
||||
<span className="text-gb-fg-f text-xs font-mono">
|
||||
{Notification.permission === 'granted' ? '● allowed' : '○ not allowed'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={isSubscribed ? unsubscribe : subscribe}
|
||||
className={`terminal-button text-xs ${isSubscribed ? 'border-gb-green text-gb-green' : 'border-gb-orange text-gb-orange'}`}
|
||||
>
|
||||
{isSubscribed ? '[DISABLE PUSH]' : '[ENABLE PUSH]'}
|
||||
</button>
|
||||
<span className="text-gb-fg-f text-xs font-mono">
|
||||
{isSubscribed ? '● subscribed' : '○ not subscribed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gb-fg-f text-xs font-mono mt-2">
|
||||
Receive push notifications for @mentions and DMs when the app is closed.
|
||||
Enable Desktop to get notifications while the app is running in the background. Enable Push to get notifications when the app is fully closed.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user