diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx index ca5d3e2..62f8e93 100644 --- a/src/auth/AuthContext.tsx +++ b/src/auth/AuthContext.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import type { PropsWithChildren } from 'react' import { clearAuthSession, getAuthSession, isAccessTokenStale, setAuthSession } from '../lib/auth-storage' import type { AuthSession } from '../lib/auth-storage' -import { loginCenter, logoutCenter, meGuildsCenter, refreshCenter } from '../lib/center-auth-client' +import { loginCenter, logoutCenter, meGuildsCenter, refreshCenter, updateMeNameCenter } from '../lib/center-auth-client' import { AuthContext } from './auth-context' import type { AuthContextValue } from './auth-context' @@ -72,6 +72,22 @@ export function AuthProvider({ children }: PropsWithChildren) { setAuthSession(next) setSession(next) }, + updateName: async (name: string) => { + if (!session) return + const accessToken = (await (async () => { + if (!isAccessTokenStale(session.accessToken)) return session.accessToken + const refreshed = await refreshCenter(session.centerApiBase, session.refreshToken) + return refreshed.accessToken + })()) + const updated = await updateMeNameCenter(session.centerApiBase, accessToken, name) + const next: AuthSession = { + ...session, + accessToken, + user: { ...session.user, name: updated.name }, + } + setAuthSession(next) + setSession(next) + }, }), [session], ) diff --git a/src/auth/auth-context.ts b/src/auth/auth-context.ts index 243e7ec..5fe4e96 100644 --- a/src/auth/auth-context.ts +++ b/src/auth/auth-context.ts @@ -8,6 +8,7 @@ export type AuthContextValue = { logout: () => Promise ensureFreshToken: () => Promise refreshGuilds: () => Promise + updateName: (name: string) => Promise } export const AuthContext = createContext(null) diff --git a/src/lib/auth-storage.ts b/src/lib/auth-storage.ts index 6cae3a3..472d62b 100644 --- a/src/lib/auth-storage.ts +++ b/src/lib/auth-storage.ts @@ -7,6 +7,7 @@ export type AuthSession = { user: { id: string email: string + name: string } guilds: Array<{ nodeId: string diff --git a/src/lib/center-auth-client.ts b/src/lib/center-auth-client.ts index 1f3b6fd..caab17c 100644 --- a/src/lib/center-auth-client.ts +++ b/src/lib/center-auth-client.ts @@ -8,7 +8,7 @@ type LoginResponse = { refreshToken: string tokenType: string expiresIn?: number - user: { id: string; email: string } + user: { id: string; email: string; name: string } guilds: Array<{ nodeId: string; name: string; endpoint: string; status: 'active' | 'offline' | 'revoked' }> guildAccessTokens: Array<{ guildNodeId: string; token: string; tokenType: string; expiresIn?: number }> } @@ -74,10 +74,23 @@ export async function guildMembersCenter( centerApiBase: string, accessToken: string, guildNodeId: string, -): Promise> { - const res = await centerClient(centerApiBase).get>( +): Promise> { + const res = await centerClient(centerApiBase).get>( `/auth/guilds/${encodeURIComponent(guildNodeId)}/members`, { headers: { Authorization: `Bearer ${accessToken}` } }, ) return Array.isArray(res.data) ? res.data : [] } + +export async function updateMeNameCenter( + centerApiBase: string, + accessToken: string, + name: string, +): Promise<{ id: string; email: string; name: string }> { + const res = await centerClient(centerApiBase).patch<{ id: string; email: string; name: string }>( + '/auth/me', + { name }, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ) + return res.data +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index c5ce19f..b7777d8 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -12,10 +12,10 @@ type MessageItem = { } type GuildChannel = { id: string; name: string; guildId?: string } -type MemberItem = { userId: string; email: string; status: string } +type MemberItem = { userId: string; email: string; name: string; status: string } export default function ChatPage() { - const { session, logout, ensureFreshToken, refreshGuilds } = useAuth() + const { session, logout, ensureFreshToken, refreshGuilds, updateName } = useAuth() const [selectedGuildId, setSelectedGuildId] = useState('') const [selectedChannelId, setSelectedChannelId] = useState('') const [channels, setChannels] = useState([]) @@ -26,7 +26,10 @@ export default function ChatPage() { const [newChannelName, setNewChannelName] = useState('') const [joinGuildNodeId, setJoinGuildNodeId] = useState('') const [selectedMemberIds, setSelectedMemberIds] = useState([]) + const [newChannelPublic, setNewChannelPublic] = useState(false) const [showCreateChannelModal, setShowCreateChannelModal] = useState(false) + const [showSettingsModal, setShowSettingsModal] = useState(false) + const [settingsName, setSettingsName] = useState('') const [showGuilds, setShowGuilds] = useState(true) const [showChannels, setShowChannels] = useState(true) const [showMembers, setShowMembers] = useState(true) @@ -153,12 +156,14 @@ export default function ChatPage() { const payload = { name: newChannelName.trim(), guildId: effectiveGuildId, + isPublic: newChannelPublic, memberUserIds: selectedMemberIds, } const res = await guildApi().post('/channels', payload) const createdId = res.data?.id as string | undefined setNewChannelName('') setSelectedMemberIds([]) + setNewChannelPublic(false) setShowCreateChannelModal(false) await loadChannels() if (createdId) setSelectedChannelId(createdId) @@ -167,6 +172,21 @@ export default function ChatPage() { } } + async function saveName() { + const trimmed = settingsName.trim() + if (!trimmed) { + setError('Name must not be empty') + return + } + try { + await updateName(trimmed) + setShowSettingsModal(false) + await loadMembers() + } catch { + setError('Failed to update name') + } + } + useEffect(() => { void loadMembers() setSelectedMemberIds([]) @@ -263,7 +283,12 @@ export default function ChatPage() {

Members

    {members.map((m) => ( -
  • {m.email}
  • +
  • + + {m.name || m.email} + {m.userId === session?.user.id ? ' (you)' : ''} + +
  • ))}
@@ -274,7 +299,16 @@ export default function ChatPage() { - + {showCreateChannelModal ? ( @@ -282,14 +316,27 @@ export default function ChatPage() {
e.stopPropagation()}>

Create channel

setNewChannelName(e.target.value)} placeholder="Channel name" /> -

Select members to include

+ +

+ {newChannelPublic ? 'You are added automatically; all guild members can see it.' : 'You are added automatically. Select members to include'} +

- {members.map((m) => ( - - ))} + {members + .filter((m) => m.userId !== session?.user.id) + .map((m) => ( + + ))}
@@ -299,6 +346,26 @@ export default function ChatPage() {
) : null} + {showSettingsModal ? ( +
setShowSettingsModal(false)}> +
e.stopPropagation()}> +

Settings

+

Signed in as {session?.user.email}

+

Display name

+ setSettingsName(e.target.value)} + placeholder="Your name" + /> +
+ + +
+
+
+ ) : null} + {error ? (
setError('')}>
e.stopPropagation()}>