diff --git a/static/js/01-api.js b/static/js/01-api.js index 1b5457c..05c8a48 100644 --- a/static/js/01-api.js +++ b/static/js/01-api.js @@ -1,6 +1,8 @@ // static/js/01-api.js "use strict"; + window.Glow = window.Glow || {}; + Glow.refreshCsrfToken = async () => { const response = await fetch("/api/csrf", { credentials: "same-origin", @@ -8,12 +10,15 @@ Glow.refreshCsrfToken = async () => { Accept: "application/json" } }); + if (!response.ok) { throw new Error("Could not refresh security token."); } + const data = await response.json(); Glow.state.csrfToken = data.csrf_token || ""; }; + Glow.handleUnauthorized = () => { Glow.state.currentUser = null; Glow.state.csrfToken = ""; @@ -46,13 +51,37 @@ Glow.handleUnauthorized = () => { Glow.state.dmRequestId += 1; Glow.state.feedRequestId += 1; Glow.state.threadRequestId += 1; - Glow.state.calls.activeCall = null; - Glow.state.calls.signalLastSeenId = 0; - Glow.state.calls.incomingCall = null; - Glow.state.calls.pendingOffer = null; + clearTimeout(Glow.state.pollTimer); clearTimeout(Glow.state.typingTimer); - clearTimeout(Glow.state.calls.pollTimer); + clearTimeout(Glow.state.calls.reconnectTimer); + clearTimeout(Glow.state.calls.invitePollTimer); + clearTimeout(Glow.state.calls.participantPollTimer); + clearTimeout(Glow.state.calls.heartbeatTimer); + + Glow.state.calls.activeCall = null; + Glow.state.calls.ws = null; + Glow.state.calls.session = null; + Glow.state.calls.localStream = null; + Glow.state.calls.muted = false; + Glow.state.calls.selfPeerId = null; + Glow.state.calls.peerConnections = new Map(); + Glow.state.calls.remoteStreams = new Map(); + Glow.state.calls.participantAudioEls = new Map(); + Glow.state.calls.peers = []; + Glow.state.calls.reconnecting = false; + Glow.state.calls.joining = false; + Glow.state.calls.leaving = false; + Glow.state.calls.invites = []; + Glow.state.calls.backendParticipants = []; + Glow.state.calls.scope = null; + Glow.state.calls.statusText = "Idle"; + Glow.state.calls.permissions = { + can_manage: false, + can_end: false, + can_invite: false + }; + Glow.clearMessages?.(); Glow.clearFeed?.(); Glow.clearDms?.(); @@ -60,41 +89,53 @@ Glow.handleUnauthorized = () => { Glow.closeReactionPopup?.(); Glow.closeMediaModal?.(); Glow.endLocalCallUi?.(); + Glow.setSelectedAttachment?.([]); Glow.setSelectedThreadAttachment?.([]); + Glow.els.threadBox?.replaceChildren(); Glow.els.pagesBox?.replaceChildren(); Glow.els.pageDetailBox?.replaceChildren(); Glow.els.profileBox?.replaceChildren(); + if (Glow.els.teamSelect) { Glow.els.teamSelect.replaceChildren(); Glow.els.teamSelect.disabled = true; } + if (Glow.els.roomSelect) { Glow.els.roomSelect.replaceChildren(); Glow.els.roomSelect.disabled = true; } + if (Glow.els.typingBar) { Glow.els.typingBar.textContent = ""; Glow.els.typingBar.classList.add("hidden"); } + if (Glow.els.presenceLabel) { Glow.els.presenceLabel.textContent = ""; Glow.els.presenceLabel.classList.add("hidden"); } + Glow.state.activeView = "feed"; + Glow.updateAuthUI?.(); Glow.showAuthNotice?.("Your session has expired. Please sign in again."); Glow.els.authOverlay.classList.remove("hidden"); }; + Glow.apiRequest = async (url, options = {}, retryCsrf = true) => { const method = (options.method || "GET").toUpperCase(); const headers = new Headers(options.headers || {}); headers.set("Accept", "application/json"); + if (!["GET", "HEAD"].includes(method)) { headers.set("X-CSRF-Token", Glow.state.csrfToken || ""); } + let response; + try { response = await fetch(url, { credentials: "same-origin", @@ -107,23 +148,29 @@ Glow.apiRequest = async (url, options = {}, retryCsrf = true) => { error.code = "network"; throw error; } + let data = {}; + try { data = await response.json(); } catch (_) { data = {}; } + if (response.status === 403 && data.code === "blacklisted") { Glow.showBlocked(data.error); throw new Error(data.error || "Access blocked."); } + if (response.status === 403 && data.code === "csrf" && retryCsrf) { await Glow.refreshCsrfToken(); return Glow.apiRequest(url, options, false); } + if (response.status === 401) { Glow.handleUnauthorized(); } + if (!response.ok) { const error = new Error(data.error || "Request failed."); error.status = response.status; @@ -133,9 +180,11 @@ Glow.apiRequest = async (url, options = {}, retryCsrf = true) => { error.data = data; throw error; } + if (data.csrf_token) { Glow.state.csrfToken = data.csrf_token; } + return data; }; @@ -147,8 +196,10 @@ Glow.apiUpdateMyProfile = payload => headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiGetPublicProfile = username => Glow.apiRequest(`/api/profiles/${encodeURIComponent(username)}`); + Glow.apiGetMySettings = () => Glow.apiRequest("/api/settings"); Glow.apiUpdateMySettings = payload => Glow.apiRequest("/api/settings", { @@ -157,54 +208,6 @@ Glow.apiUpdateMySettings = payload => body: JSON.stringify(payload) }); -/* pins / shares / bookmarks / schedule */ -Glow.apiPinMessage = messageId => - Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/pin`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - }); -Glow.apiUnpinMessage = messageId => - Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/pin`, { - method: "DELETE" - }); -Glow.apiListPins = (scope, scopeId) => - Glow.apiRequest(`/api/pins?scope=${encodeURIComponent(scope)}&scope_id=${encodeURIComponent(scopeId)}`); -Glow.apiCreateMessageShare = messageId => - Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/share`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - }); -Glow.apiCreateAttachmentShare = attachmentId => - Glow.apiRequest(`/api/attachments/${encodeURIComponent(attachmentId)}/share`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - }); -Glow.apiBookmarkMessage = messageId => - Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/bookmark`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - }); -Glow.apiUnbookmarkMessage = messageId => - Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/bookmark`, { - method: "DELETE" - }); -Glow.apiListBookmarks = () => Glow.apiRequest("/api/bookmarks"); -Glow.apiCreateScheduledMessage = payload => - Glow.apiRequest("/api/scheduled-messages", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) - }); -Glow.apiListScheduledMessages = () => Glow.apiRequest("/api/scheduled-messages"); -Glow.apiCancelScheduledMessage = id => - Glow.apiRequest(`/api/scheduled-messages/${encodeURIComponent(id)}`, { - method: "DELETE" - }); - /* teams/channels */ Glow.apiCreateTeam = payload => Glow.apiRequest("/api/teams", { @@ -212,18 +215,21 @@ Glow.apiCreateTeam = payload => headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiUpdateTeam = (teamSlug, payload) => Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiCreateChannel = (teamSlug, payload) => Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/channels`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiUpdateChannel = (teamSlug, channelSlug, payload) => Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}`, @@ -233,19 +239,23 @@ Glow.apiUpdateChannel = (teamSlug, channelSlug, payload) => body: JSON.stringify(payload) } ); + Glow.apiDeleteChannel = (teamSlug, channelSlug) => Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}`, { method: "DELETE" } ); + Glow.apiListTeamMembers = teamSlug => Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/members`); + Glow.apiAddTeamMember = (teamSlug, payload) => Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiUpdateTeamMember = (teamSlug, userId, payload) => Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/members/${encodeURIComponent(userId)}`, @@ -255,17 +265,20 @@ Glow.apiUpdateTeamMember = (teamSlug, userId, payload) => body: JSON.stringify(payload) } ); + Glow.apiRemoveTeamMember = (teamSlug, userId) => Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/members/${encodeURIComponent(userId)}`, { method: "DELETE" } ); + Glow.apiCreateTeamInvite = (teamSlug, payload = {}) => Glow.apiRequest(`/api/teams/${encodeURIComponent(teamSlug)}/invites`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiAcceptTeamInvite = token => Glow.apiRequest(`/api/team-invites/${encodeURIComponent(token)}/accept`, { method: "POST", @@ -280,6 +293,7 @@ Glow.apiCreateRoom = payload => headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiUpdateRoom = (roomSlug, payload) => Glow.apiRequest(`/api/rooms/${encodeURIComponent(roomSlug)}`, { method: "PATCH", @@ -290,12 +304,14 @@ Glow.apiUpdateRoom = (roomSlug, payload) => /* reactions */ Glow.apiGetMessageReactions = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/reactions`); + Glow.apiAddReaction = (messageId, emoji) => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/reactions`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ emoji }) }); + Glow.apiRemoveReaction = (messageId, emoji) => Glow.apiRequest( `/api/messages/${encodeURIComponent(messageId)}/reactions/${encodeURIComponent(emoji)}`, @@ -309,30 +325,38 @@ Glow.apiEditMessage = (messageId, payload) => headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiSearch = query => Glow.apiRequest(`/api/search?q=${encodeURIComponent(query)}`); + Glow.apiMarkRead = messageIds => Glow.apiRequest("/api/read-receipts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message_ids: messageIds }) }); + Glow.apiGetMessageReceipts = messageId => Glow.apiRequest(`/api/messages/${encodeURIComponent(messageId)}/receipts`); + Glow.apiPingPresence = payload => Glow.apiRequest("/api/presence", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiGetRoomPresence = roomSlug => Glow.apiRequest(`/api/rooms/${encodeURIComponent(roomSlug)}/presence`); + Glow.apiGetChannelPresence = (teamSlug, channelSlug) => Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/channels/${encodeURIComponent(channelSlug)}/presence` ); + Glow.apiGetDmPresence = dmId => Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/presence`); + Glow.apiLoadHistory = params => { const qs = new URLSearchParams(params); return Glow.apiRequest(`/api/messages/history?${qs.toString()}`); @@ -340,26 +364,31 @@ Glow.apiLoadHistory = params => { /* DMs */ Glow.apiListDms = () => Glow.apiRequest("/api/dms"); + Glow.apiCreateDm = payload => Glow.apiRequest("/api/dms", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiGetDmMessages = (dmId, afterId = null) => { const base = `/api/dms/${encodeURIComponent(dmId)}/messages`; return Glow.apiRequest( afterId == null ? base : `${base}?after_id=${encodeURIComponent(afterId)}` ); }; + Glow.apiSendDmMessage = (dmId, payload) => Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/messages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); + Glow.apiGetDmTyping = dmId => Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/typing`); + Glow.apiSetDmTyping = (dmId, typing) => Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/typing`, { method: "POST", @@ -367,28 +396,69 @@ Glow.apiSetDmTyping = (dmId, typing) => body: JSON.stringify({ typing }) }); -/* calls */ -Glow.apiCreateCall = dmId => - Glow.apiRequest(`/api/dms/${encodeURIComponent(dmId)}/calls`, { +/* group calls */ +Glow.apiGetGroupCall = sessionId => + Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}`); + +Glow.apiJoinGroupCall = sessionId => + Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/join`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); -Glow.apiGetCall = callId => - Glow.apiRequest(`/api/calls/${encodeURIComponent(callId)}`); -Glow.apiPostCallSignal = (callId, payload) => - Glow.apiRequest(`/api/calls/${encodeURIComponent(callId)}/signal`, { + +Glow.apiHeartbeatGroupCall = sessionId => + Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/heartbeat`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + }); + +Glow.apiLeaveGroupCall = sessionId => + Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/leave`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + }); + +Glow.apiEndGroupCall = sessionId => + Glow.apiRequest(`/api/group-calls/${encodeURIComponent(sessionId)}/end`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + }); + +Glow.apiGetGroupCallParticipants = sessionId => + Glow.apiRequest( + `/api/group-calls/${encodeURIComponent(sessionId)}/participants` + ); + +Glow.apiInviteToGroupCall = payload => + Glow.apiRequest("/api/group-calls/invite", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); -Glow.apiGetCallSignals = (callId, afterId = 0) => + +Glow.apiListGroupCallInvites = () => + Glow.apiRequest("/api/group-calls/invites"); + +Glow.apiAcceptGroupCallInvite = inviteId => Glow.apiRequest( - `/api/calls/${encodeURIComponent(callId)}/signals?after_id=${encodeURIComponent(afterId)}` + `/api/group-calls/invites/${encodeURIComponent(inviteId)}/accept`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + } ); -Glow.apiEndCall = callId => - Glow.apiRequest(`/api/calls/${encodeURIComponent(callId)}/end`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}) - }); \ No newline at end of file + +Glow.apiDismissGroupCallInvite = inviteId => + Glow.apiRequest( + `/api/group-calls/invites/${encodeURIComponent(inviteId)}/dismiss`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + } + ); \ No newline at end of file