greatly overhaul overlay system

This commit is contained in:
hippoz 2022-09-03 17:30:39 +03:00
parent d8552c0328
commit 4c9e321167
Signed by: hippoz
GPG key ID: 7C52899193467641
16 changed files with 117 additions and 117 deletions

View file

@ -1,15 +1,15 @@
import gateway, { GatewayEventType } from "./gateway"; import gateway, { GatewayEventType } from "./gateway";
import { removeItem, setItem } from "./storage"; import { removeItem, setItem } from "./storage";
import { overlayStore } from "./stores"; import { overlayStore, OverlayType } from "./stores";
export function useAuthHandlers() { export function useAuthHandlers() {
gateway.subscribe(GatewayEventType.Ready, () => { gateway.subscribe(GatewayEventType.Ready, () => {
overlayStore.close("login"); overlayStore.popType(OverlayType.Login);
overlayStore.close("createAccount"); overlayStore.popType(OverlayType.CreateAccount);
}); });
gateway.subscribe(GatewayEventType.BadAuth, () => { gateway.subscribe(GatewayEventType.BadAuth, () => {
overlayStore.open("login", {}); overlayStore.push(OverlayType.Login, {});
}); });
} }

View file

@ -1,7 +1,7 @@
<script> <script>
import { HashIcon, MenuIcon, UsersIcon } from "svelte-feather-icons"; import { HashIcon, MenuIcon, UsersIcon } from "svelte-feather-icons";
import { getItem } from "../storage"; import { getItem } from "../storage";
import { overlayStore, showPresenceSidebar, showSidebar } from "../stores"; import { overlayStore, OverlayType, showPresenceSidebar, showSidebar } from "../stores";
export let channel; export let channel;
</script> </script>
@ -24,7 +24,7 @@
</button> </button>
{/if} {/if}
<HashIcon /> <HashIcon />
<span class="h5 top-bar-heading" on:click="{ () => overlayStore.open('editChannel', {channel}) }">{ channel.name }</span> <span class="h5 top-bar-heading" on:click="{ () => overlayStore.push(OverlayType.EditChannel, {channel}) }">{ channel.name }</span>
<div class="right-buttons"> <div class="right-buttons">
<button class="icon-button" on:click="{ () => showPresenceSidebar.set(!showPresenceSidebar.value) }" aria-label="Toggle user list"> <button class="icon-button" on:click="{ () => showPresenceSidebar.set(!showPresenceSidebar.value) }" aria-label="Toggle user list">
<UsersIcon /> <UsersIcon />

View file

@ -1,6 +1,6 @@
<script> <script>
import { CornerUpLeftIcon, MoreVerticalIcon } from "svelte-feather-icons"; import { CornerUpLeftIcon, MoreVerticalIcon } from "svelte-feather-icons";
import { overlayStore, setMessageInputEvent, userInfoStore } from "../stores"; import { overlayStore, OverlayType, setMessageInputEvent, userInfoStore } from "../stores";
export let message; export let message;
@ -71,7 +71,7 @@
<CornerUpLeftIcon /> <CornerUpLeftIcon />
</button> </button>
{#if userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)} {#if userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)}
<button class="icon-button edit-message" on:click="{ () => overlayStore.open('editMessage', { message }) }" aria-label="Edit Message"> <button class="icon-button edit-message" on:click="{ () => overlayStore.push(OverlayType.EditMessage, { message }) }" aria-label="Edit Message">
<MoreVerticalIcon /> <MoreVerticalIcon />
</button> </button>
{/if} {/if}

View file

@ -71,9 +71,7 @@
messages.deleteMessage({ messages.deleteMessage({
id: optimisticMessageId id: optimisticMessageId
}); });
overlayStore.open("toast", { overlayStore.toast("Couldn't send message");
message: "Couldn't send message"
});
} }
}; };

View file

@ -2,7 +2,7 @@
import { HashIcon, PlusIcon, MoreVerticalIcon, SettingsIcon, CloudIcon } from "svelte-feather-icons"; import { HashIcon, PlusIcon, MoreVerticalIcon, SettingsIcon, CloudIcon } from "svelte-feather-icons";
import { quadInOut } from "svelte/easing"; import { quadInOut } from "svelte/easing";
import { maybeFly, maybeFlyIf } from "../animations"; import { maybeFly, maybeFlyIf } from "../animations";
import { channels, gatewayStatus, overlayStore, selectedChannel, showSidebar, smallViewport, userInfoStore, unreadStore } from "../stores"; import { channels, gatewayStatus, overlayStore, selectedChannel, showSidebar, smallViewport, userInfoStore, unreadStore, OverlayType } from "../stores";
import UserTopBar from "./UserTopBar.svelte"; import UserTopBar from "./UserTopBar.svelte";
const selectChannel = (channel) => { const selectChannel = (channel) => {
@ -28,7 +28,7 @@
<div class="unread-indicator">{ $unreadStore.get(channel.id) }</div> <div class="unread-indicator">{ $unreadStore.get(channel.id) }</div>
{/if} {/if}
{#if $userInfoStore && (channel.owner_id === $userInfoStore.id || $userInfoStore.is_superuser)} {#if $userInfoStore && (channel.owner_id === $userInfoStore.id || $userInfoStore.is_superuser)}
<button class="icon-button" on:click|stopPropagation="{ () => overlayStore.open('editChannel', { channel }) }" aria-label="Edit Channel"> <button class="icon-button" on:click|stopPropagation="{ () => overlayStore.push(OverlayType.EditChannel, { channel }) }" aria-label="Edit Channel">
<MoreVerticalIcon /> <MoreVerticalIcon />
</button> </button>
{/if} {/if}
@ -36,14 +36,14 @@
</button> </button>
{/each} {/each}
{#if $userInfoStore && $userInfoStore.permissions.create_channel} {#if $userInfoStore && $userInfoStore.permissions.create_channel}
<button on:click="{ () => overlayStore.open('createChannel') }" class="sidebar-button"> <button on:click="{ () => overlayStore.push(OverlayType.CreateChannel) }" class="sidebar-button">
<div class="sidebar-button-icon"> <div class="sidebar-button-icon">
<PlusIcon /> <PlusIcon />
</div> </div>
<span>Create Channel</span> <span>Create Channel</span>
</button> </button>
{/if} {/if}
<button on:click="{ () => overlayStore.open('settings') }" class="sidebar-button"> <button on:click="{ () => overlayStore.push(OverlayType.Settings) }" class="sidebar-button">
<div class="sidebar-button-icon"> <div class="sidebar-button-icon">
<SettingsIcon /> <SettingsIcon />
</div> </div>

View file

@ -1,5 +1,5 @@
<script> <script>
import { overlayStore } from "../../stores"; import { overlayStore, OverlayType } from "../../stores";
import request from "../../request"; import request from "../../request";
import { apiRoute } from "../../storage"; import { apiRoute } from "../../storage";
import { maybeModalFly } from "../../animations"; import { maybeModalFly } from "../../animations";
@ -8,8 +8,8 @@
let password = ""; let password = "";
let buttonsEnabled = true; let buttonsEnabled = true;
let pendingOtherOpen = false; let pendingOtherOpen = false;
export let close = () => {};
const close = () => overlayStore.close('createAccount');
const create = async () => { const create = async () => {
buttonsEnabled = false; buttonsEnabled = false;
const { ok } = await request("POST", apiRoute("users/register"), false, { const { ok } = await request("POST", apiRoute("users/register"), false, {
@ -17,14 +17,10 @@
password password
}); });
if (ok) { if (ok) {
overlayStore.open("toast", { overlayStore.toast("Account created");
message: "Account created"
});
loginInstead(); loginInstead();
} else { } else {
overlayStore.open("toast", { overlayStore.toast("Couldn't create account");
message: "Couldn't create account"
});
buttonsEnabled = true; buttonsEnabled = true;
return; return;
} }
@ -35,7 +31,7 @@
} }
const outroEnd = () => { const outroEnd = () => {
if (pendingOtherOpen) { if (pendingOtherOpen) {
overlayStore.open("login", {}); overlayStore.push(OverlayType.Login);
} }
}; };
const onKeydown = async (e) => { const onKeydown = async (e) => {

View file

@ -6,17 +6,15 @@
let channelName = ""; let channelName = "";
let createButtonEnabled = true; let createButtonEnabled = true;
export let close = () => {};
const close = () => overlayStore.close('createChannel');
const create = async () => { const create = async () => {
createButtonEnabled = false; createButtonEnabled = false;
const { ok } = await request("POST", apiRoute("channels"), true, { const { ok } = await request("POST", apiRoute("channels"), true, {
name: channelName name: channelName
}); });
if (!ok) { if (!ok) {
overlayStore.open("toast", { overlayStore.toast("Couldn't create channel");
message: "Couldn't create channel"
});
} }
close(); close();
}; };

View file

@ -8,17 +8,15 @@
let channelName = channel.name; let channelName = channel.name;
let buttonsEnabled = true; let buttonsEnabled = true;
export let close = () => {};
const close = () => overlayStore.close('editChannel');
const save = async () => { const save = async () => {
buttonsEnabled = false; buttonsEnabled = false;
const { ok } = await request("PUT", apiRoute(`channels/${channel.id}`), true, { const { ok } = await request("PUT", apiRoute(`channels/${channel.id}`), true, {
name: channelName name: channelName
}); });
if (!ok) { if (!ok) {
overlayStore.open("toast", { overlayStore.toast("Couldn't edit channel");
message: "Couldn't edit channel"
});
} }
close(); close();
}; };
@ -26,9 +24,7 @@
buttonsEnabled = false; buttonsEnabled = false;
const { ok } = await request("DELETE", apiRoute(`channels/${channel.id}`), true); const { ok } = await request("DELETE", apiRoute(`channels/${channel.id}`), true);
if (!ok) { if (!ok) {
overlayStore.open("toast", { overlayStore.toast("Couldn't delete channel");
message: "Couldn't delete channel"
});
} }
close(); close();
}; };

View file

@ -1,5 +1,4 @@
<script> <script>
import { quintInOut } from "svelte/easing";
import { overlayStore } from "../../stores"; import { overlayStore } from "../../stores";
import request from "../../request"; import request from "../../request";
import { apiRoute } from "../../storage"; import { apiRoute } from "../../storage";
@ -9,17 +8,15 @@
let messageContent = message.content; let messageContent = message.content;
let buttonsEnabled = true; let buttonsEnabled = true;
export let close = () => {};
const close = () => overlayStore.close('editMessage');
const save = async () => { const save = async () => {
buttonsEnabled = false; buttonsEnabled = false;
const { ok } = await request("PUT", apiRoute(`messages/${message.id}`), true, { const { ok } = await request("PUT", apiRoute(`messages/${message.id}`), true, {
content: messageContent content: messageContent
}); });
if (!ok) { if (!ok) {
overlayStore.open("toast", { overlayStore.toast("Couldn't edit message");
message: "Couldn't edit message"
});
} }
close(); close();
}; };
@ -27,9 +24,7 @@
buttonsEnabled = false; buttonsEnabled = false;
const { ok } = await request("DELETE", apiRoute(`messages/${message.id}`), true); const { ok } = await request("DELETE", apiRoute(`messages/${message.id}`), true);
if (!ok) { if (!ok) {
overlayStore.open("toast", { overlayStore.toast("Couldn't delete message");
message: "Couldn't delete message"
});
} }
close(); close();
}; };

View file

@ -1,5 +1,5 @@
<script> <script>
import { overlayStore } from "../../stores"; import { overlayStore, OverlayType } from "../../stores";
import request from "../../request"; import request from "../../request";
import { apiRoute } from "../../storage"; import { apiRoute } from "../../storage";
import { authWithToken } from "../../auth"; import { authWithToken } from "../../auth";
@ -9,8 +9,8 @@
let password = ""; let password = "";
let buttonsEnabled = true; let buttonsEnabled = true;
let pendingOtherOpen = false; let pendingOtherOpen = false;
export let close = () => {};
const close = () => overlayStore.close('login');
const login = async () => { const login = async () => {
buttonsEnabled = false; buttonsEnabled = false;
const { ok, json } = await request("POST", apiRoute("users/login"), false, { const { ok, json } = await request("POST", apiRoute("users/login"), false, {
@ -21,13 +21,9 @@
authWithToken(json.token, true); authWithToken(json.token, true);
} else { } else {
if (json && json.code && json.code === 6002) { // 6002 is the code for bad login if (json && json.code && json.code === 6002) { // 6002 is the code for bad login
overlayStore.open("toast", { overlayStore.toast("Invalid username or password");
message: "Invalid username or password"
});
} else { } else {
overlayStore.open("toast", { overlayStore.toast("Couldn't log in");
message: "Couldn't log in"
});
} }
buttonsEnabled = true; buttonsEnabled = true;
return; return;
@ -39,7 +35,7 @@
}; };
const outroEnd = () => { const outroEnd = () => {
if (pendingOtherOpen) { if (pendingOtherOpen) {
overlayStore.open("createAccount", {}); overlayStore.push(OverlayType.CreateAccount);
} }
}; };
const onKeydown = async (e) => { const onKeydown = async (e) => {

View file

@ -1,5 +1,6 @@
<script> <script>
import { overlayStore } from "../../stores"; import { overlayStore } from "../../stores";
import EditChannel from "./EditChannel.svelte"; import EditChannel from "./EditChannel.svelte";
import CreateChannel from "./CreateChannel.svelte"; import CreateChannel from "./CreateChannel.svelte";
import Toast from "./Toast.svelte"; import Toast from "./Toast.svelte";
@ -8,29 +9,19 @@
import EditMessage from "./EditMessage.svelte"; import EditMessage from "./EditMessage.svelte";
import Settings from "./Settings.svelte"; import Settings from "./Settings.svelte";
import Prompt from "./Prompt.svelte"; import Prompt from "./Prompt.svelte";
const OverlayComponent = {
0: CreateChannel,
1: EditChannel,
2: Toast,
3: Login,
4: CreateAccount,
5: EditMessage,
6: Settings,
7: Prompt,
};
</script> </script>
{#if $overlayStore.createChannel} {#each $overlayStore as overlay (overlay.id)}
<CreateChannel /> <svelte:component this={ OverlayComponent[overlay.type] } {...overlay.props} />
{/if} {/each}
{#if $overlayStore.editChannel}
<EditChannel { ...$overlayStore.editChannel } />
{/if}
{#if $overlayStore.toast}
<Toast { ...$overlayStore.toast } />
{/if}
{#if $overlayStore.login}
<Login />
{/if}
{#if $overlayStore.createAccount}
<CreateAccount />
{/if}
{#if $overlayStore.editMessage}
<EditMessage { ...$overlayStore.editMessage } />
{/if}
{#if $overlayStore.settings}
<Settings />
{/if}
{#if $overlayStore.prompt}
<Prompt { ...$overlayStore.prompt } />
{/if}

View file

@ -1,6 +1,5 @@
<script> <script>
import { maybeModalFade, maybeModalFly } from "../../animations"; import { maybeModalFade, maybeModalFly } from "../../animations";
import { overlayStore } from "../../stores";
export let onSubmit = async () => {}; export let onSubmit = async () => {};
export let onClose = async () => {}; export let onClose = async () => {};
@ -9,15 +8,16 @@
let userInput = ""; let userInput = "";
let buttonsEnabled = true; let buttonsEnabled = true;
export let close = () => {};
const close = async () => { const closePrompt = async () => {
await onClose(); await onClose();
overlayStore.close("prompt"); close();
}; };
const save = async () => { const save = async () => {
buttonsEnabled = false; buttonsEnabled = false;
await onSubmit(userInput); await onSubmit(userInput);
close(); closePrompt();
}; };
const onKeydown = async (e) => { const onKeydown = async (e) => {
if (e.code !== "Enter") if (e.code !== "Enter")
@ -33,7 +33,7 @@
} }
</style> </style>
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }"> <div class="modal-backdrop" transition:maybeModalFade on:click="{ closePrompt }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeModalFly on:click|stopPropagation> <div class="modal" transition:maybeModalFly on:click|stopPropagation>
<div class="modal-header"> <div class="modal-header">
<span class="h4">{ heading }</span> <span class="h4">{ heading }</span>
@ -45,7 +45,7 @@
</label> </label>
<div class="modal-footer"> <div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button> <button class="button modal-secondary-action" on:click="{ closePrompt }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Submit</button> <button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Submit</button>
</div> </div>
</div> </div>

View file

@ -1,15 +1,15 @@
<script> <script>
import { AtSignIcon } from "svelte-feather-icons"; import { AtSignIcon } from "svelte-feather-icons";
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations } from "../../stores"; import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType } from "../../stores";
import { logOut } from "../../auth"; import { logOut } from "../../auth";
import { maybeModalFade, maybeModalFly } from "../../animations"; import { maybeModalFade, maybeModalFly } from "../../animations";
import request from "../../request"; import request from "../../request";
import { apiRoute, getItem } from "../../storage"; import { apiRoute, getItem } from "../../storage";
const close = () => overlayStore.close("settings"); export let close = () => {};
const doDeveloper = () => { const doDeveloper = () => {
overlayStore.open("prompt", { overlayStore.push(OverlayType.Prompt, {
heading: "", heading: "",
valueName: "", valueName: "",
async onSubmit(value) { async onSubmit(value) {
@ -19,8 +19,7 @@
} }
const respond = (value) => { const respond = (value) => {
overlayStore.close("prompt"); overlayStore.toast(value);
overlayStore.open("toast", { message: value });
}; };
switch (parts[0]) { switch (parts[0]) {
@ -38,17 +37,15 @@
const doLogout = () => { const doLogout = () => {
close(); close();
logOut(); logOut();
overlayStore.open("toast", { overlayStore.toast("Logged out");
message: "Logged out"
});
}; };
const doSuperuserPrompt = async () => { const doSuperuserPrompt = async () => {
const { ok } = await request("POST", apiRoute("users/self/promote"), true); const { ok } = await request("POST", apiRoute("users/self/promote"), true);
if (ok) { if (ok) {
overlayStore.open("toast", { message: "You have been promoted to superuser" }); overlayStore.toast("You have been promoted to superuser");
} else { } else {
overlayStore.open("toast", { message: "Failed to promote to superuser" }); overlayStore.toast("Failed to promote to superuser");
} }
}; };
</script> </script>

View file

@ -4,6 +4,7 @@
import { overlayStore } from "../../stores"; import { overlayStore } from "../../stores";
export let message; export let message;
export let close = () => {};
</script> </script>
<style> <style>
@ -25,7 +26,7 @@
{#key message} {#key message}
<div class="toast" transition:maybeModalFly> <div class="toast" transition:maybeModalFly>
<span>{ message }</span> <span>{ message }</span>
<button class="icon-button icon-button-auto" on:click="{ () => overlayStore.close('toast') }"> <button class="icon-button icon-button-auto" on:click="{ close }">
<XIcon /> <XIcon />
</button> </button>
</div> </div>

View file

@ -1,6 +1,6 @@
import { getItem } from "./storage"; import { getItem } from "./storage";
// TODO: circular dependency // TODO: circular dependency
import { overlayStore } from "./stores"; import { overlayStore, OverlayType } from "./stores";
export function compatibleFetch(endpoint, options) { export function compatibleFetch(endpoint, options) {
if (window.fetch && typeof window.fetch === "function") { if (window.fetch && typeof window.fetch === "function") {
@ -62,7 +62,7 @@ export default function doRequest(method, endpoint, auth=true, body=null, _keyEn
if (res.status === 403 && json.code && json.code === 6006 && !_keyEntryDepth) { if (res.status === 403 && json.code && json.code === 6006 && !_keyEntryDepth) {
// This endpoint is password-protected // This endpoint is password-protected
overlayStore.open("prompt", { overlayStore.push(OverlayType.Prompt, {
heading: "Enter Key For Resource", heading: "Enter Key For Resource",
valueName: "Key", valueName: "Key",
async onSubmit(value) { async onSubmit(value) {

View file

@ -234,9 +234,7 @@ class MessageStore extends Store {
this.value = res.json.concat(this.value); this.value = res.json.concat(this.value);
this.updated(); this.updated();
} else { } else {
overlayStore.open("toast", { overlayStore.toast("Messages failed to load");
message: "Messages failed to load"
});
} }
} }
@ -284,34 +282,68 @@ class MessagesStoreProvider {
} }
} }
export const OverlayType = {
CreateChannel: 0,
EditChannel: 1,
Toast: 2,
Login: 3,
CreateAccount: 4,
EditMessage: 5,
Settings: 6,
Prompt: 7,
};
class OverlayStore extends Store { class OverlayStore extends Store {
constructor() { constructor() {
super({ super([], "OverlayStore");
createChannel: null,
editChannel: null,
toast: null,
login: null,
createAccount: null,
editMessage: null,
settings: null,
prompt: null,
}, "OverlayStore");
} }
open(name, props={}) { push(type, props={}) {
if (this.value[name] === undefined) const id = Math.floor(Math.random() * 9999999);
throw new Error(`OverlayStore.open: tried to open unknown overlay with name '${name}' (undefined in overlay map)`);
this.value[name] = props; props = {
...props,
close: () => {
this.popId(id);
}
}
this.value.push({
type,
props,
id
});
this.updated(); this.updated();
} }
close(name) { pop() {
if (!this.value[name]) this.value.pop();
return;
this.value[name] = null;
this.updated(); this.updated();
} }
popType(type) {
for (let i = this.value.length - 1; i >= 0; i--) {
if (this.value[i].type === type) {
this.value.splice(i, 1);
this.updated();
return;
}
}
}
popId(id) {
for (let i = this.value.length - 1; i >= 0; i--) {
if (this.value[i].id === id) {
this.value.splice(i, 1);
this.updated();
return;
}
}
}
toast(message) {
this.push(OverlayType.Toast, { message });
}
} }
class TypingStore extends Store { class TypingStore extends Store {