Compare commits
6 commits
b1a4622151
...
b100f13640
Author | SHA1 | Date | |
---|---|---|---|
|
b100f13640 | ||
|
1bd0351f1b | ||
|
dc3191b187 | ||
|
08f82f5845 | ||
|
3740d764a7 | ||
|
745e10b6bb |
29 changed files with 389 additions and 213 deletions
|
@ -11,11 +11,7 @@ class Bridge {
|
||||||
intents: 0 | (1 << 0) | (1 << 9) | (1 << 15), // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
|
intents: 0 | (1 << 0) | (1 << 9) | (1 << 15), // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.waffleClient = new WaffleClient(WAFFLE_TOKEN, {
|
this.waffleClient = new WaffleClient(WAFFLE_TOKEN, {});
|
||||||
bridgesTo: "Discord Inc. (not affiliated)",
|
|
||||||
privacy: "https://discord.com/privacy",
|
|
||||||
terms: "https://discord.com/terms"
|
|
||||||
});
|
|
||||||
this.waffleChannelIdToDiscordChannelIdMap = new Map();
|
this.waffleChannelIdToDiscordChannelIdMap = new Map();
|
||||||
this.discordChannelIdToWaffleChannelIdMap = new Map();
|
this.discordChannelIdToWaffleChannelIdMap = new Map();
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<span class="unread-indicator">{$totalUnreadsStore}</span>
|
<span class="unread-indicator">{$totalUnreadsStore}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="material-icons-outlined" class:tag-icon-has-sidebar-button="{ !$showSidebar }" class:has-unreads="{ $totalUnreadsStore > 0 }">tag</span>
|
<span class="material-icons-outlined" class:tag-icon-has-sidebar-button="{ !$showSidebar }" class:has-unreads="{ $totalUnreadsStore > 0 }">tag</span>
|
||||||
<span class="text-small top-bar-heading accent" on:click="{ () => overlayStore.push(OverlayType.EditChannel, {channel}) }">{ channel.name }</span>
|
<span class="text-small top-bar-heading accent" on:click|stopPropagation="{ ({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.EditChannel, pageX, pageY, {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">
|
||||||
<span class="material-icons-outlined">people</span>
|
<span class="material-icons-outlined">people</span>
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
onSelect(selectedOptionId);
|
onSelect(selectedOptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionClick = (option) => {
|
const optionClick = (option, event) => {
|
||||||
if (option) {
|
if (option) {
|
||||||
if (doHighlight) {
|
if (doHighlight) {
|
||||||
selectedOptionId = option.id;
|
selectedOptionId = option.id;
|
||||||
}
|
}
|
||||||
onSelect(selectedOptionId);
|
onSelect(selectedOptionId);
|
||||||
if (option.handle) {
|
if (option.handle) {
|
||||||
option.handle();
|
option.handle(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -45,6 +45,12 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smaller button {
|
||||||
|
font-size: 0.85em;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--background-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--background-color-3);
|
background-color: var(--background-color-3);
|
||||||
}
|
}
|
||||||
|
@ -65,17 +71,13 @@
|
||||||
.has-text .material-icons-outlined {
|
.has-text .material-icons-outlined {
|
||||||
margin-right: var(--space-xxs);
|
margin-right: var(--space-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.smaller button {
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<div class:smaller={smaller}>
|
<div class:smaller={smaller}>
|
||||||
{#each options as option (option.id)}
|
{#each options as option (option.id)}
|
||||||
{#if !option.hidden}
|
{#if !option.hidden}
|
||||||
<button class="button" class:selected={ selectedOptionId === option.id } class:has-text={!!option.text} on:click={ optionClick(option) }>
|
<button class="button" class:selected={ selectedOptionId === option.id } class:has-text={!!option.text} on:click|stopPropagation="{ e => optionClick(option, e) }">
|
||||||
{#if option.icon}
|
{#if option.icon}
|
||||||
<span class="material-icons-outlined">{ option.icon }</span>
|
<span class="material-icons-outlined">{ option.icon }</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
54
frontend/src/components/EditableText.svelte
Normal file
54
frontend/src/components/EditableText.svelte
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
export let value = "";
|
||||||
|
export let fieldName = "Text";
|
||||||
|
export let onSave = (_) => {};
|
||||||
|
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
let showEditButton = false;
|
||||||
|
let currentValue = value;
|
||||||
|
|
||||||
|
$: showEditButton = (value !== currentValue);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--h4);
|
||||||
|
line-height: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
color: currentColor;
|
||||||
|
padding: var(--space-xxs);
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible {
|
||||||
|
outline: 1px solid var(--background-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
margin-left: var(--space-xxs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input bind:value={ currentValue } type="text" name="{ fieldName }" disabled={ disabled }>
|
||||||
|
{#if !disabled && showEditButton}
|
||||||
|
<button class="icon-button material-icons-outlined" on:click="{ () => onSave(currentValue) }" aria-label="Save">
|
||||||
|
edit
|
||||||
|
</button>
|
||||||
|
<button class="icon-button material-icons-outlined" on:click="{ () => currentValue = value }" aria-label="Reset">
|
||||||
|
refresh
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
29
frontend/src/components/FetchedUserView.svelte
Normal file
29
frontend/src/components/FetchedUserView.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { getGrantFor } from "../permissions";
|
||||||
|
import { methods, remoteCall, responseOk } from "../request";
|
||||||
|
import UserView from "./UserView.svelte";
|
||||||
|
|
||||||
|
export let userId;
|
||||||
|
export let capabilityType = 0;
|
||||||
|
export let capabilityResource = null;
|
||||||
|
export let showBadges = true;
|
||||||
|
|
||||||
|
let grant = 0;
|
||||||
|
|
||||||
|
let userInfoPromise = remoteCall(methods.getUser, userId).then(response => {
|
||||||
|
if (!responseOk(response)) {
|
||||||
|
throw new Error("Failed to get user info");
|
||||||
|
}
|
||||||
|
grant = capabilityType && capabilityResource ? getGrantFor(response.data, capabilityType, capabilityResource) : 0;
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#await userInfoPromise}
|
||||||
|
<span class="text-fg-3 text-bold text-small">Loading...</span>
|
||||||
|
{:then user}
|
||||||
|
<UserView size="28" {user} {grant} {showBadges} />
|
||||||
|
{:catch}
|
||||||
|
<span class="text-fg-3 text-bold text-small">Failed to load user info</span>
|
||||||
|
{/await}
|
|
@ -92,12 +92,14 @@
|
||||||
<img loading="lazy" decoding="async" width="{ attachment.width }" height="{ attachment.height }" class="attachment media" alt="Attachment" src="{ attachmentUrl(attachment.file) }">
|
<img loading="lazy" decoding="async" width="{ attachment.width }" height="{ attachment.height }" class="attachment media" alt="Attachment" src="{ attachmentUrl(attachment.file) }">
|
||||||
{:else if renderAs === AttachmentRenderAs.Video}
|
{:else if renderAs === AttachmentRenderAs.Video}
|
||||||
<!-- svelte-ignore a11y-media-has-caption -->
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
<video controls="controls" class="attachment media" src="{ attachmentUrl(attachment.file) }"></video>
|
<video controls="controls" class="attachment media" src="{ attachmentUrl(attachment.file) }" width=400 height=400></video>
|
||||||
{:else if renderAs === AttachmentRenderAs.DownloadableFile}
|
{:else if renderAs === AttachmentRenderAs.DownloadableFile}
|
||||||
<div class="attachment attachment-card">
|
<div class="attachment attachment-card">
|
||||||
<div class="attachment-filename">{ attachment.file_name }</div>
|
<div class="attachment-filename">{ attachment.file_name }</div>
|
||||||
<a class="icon-button material-icons-outlined small download" href="{ attachmentUrl(attachment.file) }" target="_blank">download</a>
|
<a class="icon-button material-icons-outlined small download" href="{ attachmentUrl(attachment.file) }" target="_blank">download</a>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if renderAs === AttachmentRenderAs.Audio}
|
||||||
|
<audio controls="controls" class="attachment media" src="{ attachmentUrl(attachment.file) }"></audio>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="attachment attachment-card">Couldn't render attachment</div>
|
<div class="attachment attachment-card">Couldn't render attachment</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -23,6 +23,10 @@
|
||||||
min-width: 248px;
|
min-width: 248px;
|
||||||
max-width: 248px;
|
max-width: 248px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-button {
|
||||||
|
color: var(--foreground-color-2);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="sidebar-container" class:presence-sidebar-limited="{ !$smallViewport }" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
<div class="sidebar-container" class:presence-sidebar-limited="{ !$smallViewport }" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
||||||
|
@ -33,9 +37,6 @@
|
||||||
{#each $presenceStore as entry (entry.user.id)}
|
{#each $presenceStore as entry (entry.user.id)}
|
||||||
<button class="sidebar-button">
|
<button class="sidebar-button">
|
||||||
<UserView size={28} user={entry.user}></UserView>
|
<UserView size={28} user={entry.user}></UserView>
|
||||||
{#if entry.bridgesTo || entry.privacy || entry.terms}
|
|
||||||
<button class="user-badge" on:click={ () => overlayStore.push(OverlayType.UserInfo, { presenceEntry: entry }) }>SERVICE</button>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if $smallViewport}
|
{#if $smallViewport}
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
<button class="button" on:click={() => overlayStore.push(OverlayType.AddCommunity)}>
|
<button class="button" on:click|stopPropagation={({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.AddCommunity, pageX, pageY)}>
|
||||||
<span class="material-icons-outlined">add</span>
|
<span class="material-icons-outlined">add</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button" on:click={() => overlayStore.push(OverlayType.Settings)}>
|
<button class="button" on:click={() => overlayStore.push(OverlayType.Settings)}>
|
||||||
|
@ -109,11 +109,9 @@
|
||||||
{#if $unreadStore.get(channel.id)}
|
{#if $unreadStore.get(channel.id)}
|
||||||
<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)}
|
<button class="icon-button" on:click|stopPropagation="{ ({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.EditChannel, pageX, pageY, { channel }) }" aria-label="Edit Channel">
|
||||||
<button class="icon-button" on:click|stopPropagation="{ () => overlayStore.push(OverlayType.EditChannel, { channel }) }" aria-label="Edit Channel">
|
|
||||||
<span class="material-icons-outlined">more_vert</span>
|
<span class="material-icons-outlined">more_vert</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { CapabilityType, getGrantFor } from "../permissions";
|
||||||
import { avatarUrl } from "../storage";
|
import { avatarUrl } from "../storage";
|
||||||
import { OverlayType, overlayStore, userInfoStore } from "../stores";
|
import { OverlayType, overlayStore, userInfoStore } from "../stores";
|
||||||
import ChipBar from "./ChipBar.svelte";
|
import ChipBar from "./ChipBar.svelte";
|
||||||
|
@ -17,18 +18,18 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "EDIT_COMMUNITY",
|
id: "EDIT_COMMUNITY",
|
||||||
icon: "edit",
|
icon: "more_vert",
|
||||||
hidden: !(!isUser && communityLike && $userInfoStore && communityLike.owner_id === $userInfoStore.id),
|
hidden: isUser || !communityLike,
|
||||||
handle() {
|
handle({ pageX, pageY }) {
|
||||||
overlayStore.push(OverlayType.EditCommunity, { community: communityLike });
|
overlayStore.pushAbsolute(OverlayType.EditCommunity, pageX, pageY, { community: communityLike });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "NEW_CHANNEL",
|
id: "NEW_CHANNEL",
|
||||||
icon: "add",
|
icon: "add",
|
||||||
hidden: (!$userInfoStore || !$userInfoStore.permissions.create_channel),
|
hidden: (!$userInfoStore || !$userInfoStore.permissions.create_channel),
|
||||||
handle() {
|
handle({ pageX, pageY }) {
|
||||||
overlayStore.push(OverlayType.CreateChannel, { community: isUser ? { id: -1 } : communityLike });
|
overlayStore.pushAbsolute(OverlayType.CreateChannel, pageX, pageY, { community: isUser ? { id: -1 } : communityLike });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { CapabilityGrant } from "../permissions";
|
||||||
import { avatarUrl } from "../storage";
|
import { avatarUrl } from "../storage";
|
||||||
|
|
||||||
|
|
||||||
export let user = null;
|
export let user = null;
|
||||||
export let size = 32;
|
export let size = 32;
|
||||||
|
export let showBadges = true;
|
||||||
|
export let grant = CapabilityGrant.None;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
font-size: var(--h6);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -30,9 +34,19 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if user && user.avatar}
|
{#if user && user.avatar}
|
||||||
<img src={avatarUrl(user.avatar, size)} alt=" ">
|
<img src={avatarUrl(user.avatar, size)} width={size} height={size} alt=" ">
|
||||||
{:else}
|
{:else}
|
||||||
<span class="material-icons-outlined circled-icon">alternate_email</span>
|
<span class="material-icons-outlined circled-icon" style="width: {size}px; height: {size}px;">alternate_email</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="username">{ user ? user.username : "" }</span>
|
<span class="username">{ user ? user.username : "" }</span>
|
||||||
|
{#if showBadges}
|
||||||
|
{#if user.is_superuser}
|
||||||
|
<span class="user-badge secondary">Superuser</span>
|
||||||
|
{/if}
|
||||||
|
{#if grant === CapabilityGrant.ResourceOwner}
|
||||||
|
<span class="user-badge secondary">Owner</span>
|
||||||
|
{:else if grant === CapabilityGrant.ResourceManager}
|
||||||
|
<span class="user-badge secondary">Manager</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
|
@ -7,6 +7,7 @@
|
||||||
let createButtonEnabled = true;
|
let createButtonEnabled = true;
|
||||||
let response;
|
let response;
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
|
export let place = null;
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
createButtonEnabled = false;
|
createButtonEnabled = false;
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {close} enter={create}>
|
<Modal {place} {close} enter={create} showCloseButton={false}>
|
||||||
<span class="h4" slot="header">Create Community</span>
|
<span class="h4" slot="header">Create Community</span>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
@ -30,7 +31,6 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
|
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal opaque close={loginInstead} {outroEnd} enter={create}>
|
<Modal opaque {outroEnd} enter={create} showCloseButton={false}>
|
||||||
<span class="h4" slot="header">Create an Account</span>
|
<span class="h4" slot="header">Create an Account</span>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="button modal-secondary-action" on:click="{ loginInstead }" disabled="{ !buttonsEnabled }">Log in instead</button>
|
<button class="hyperlink-button padded" on:click="{ loginInstead }" disabled="{ !buttonsEnabled }">Log in instead</button>
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
|
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
let response;
|
let response;
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
export let community = null;
|
export let community = null;
|
||||||
|
export let place = null;
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
createButtonEnabled = false;
|
createButtonEnabled = false;
|
||||||
|
@ -18,12 +19,14 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {close} enter={create}>
|
<Modal {place} {close} enter={create} showCloseButton={false}>
|
||||||
<svelte:fragment slot="header">
|
<svelte:fragment slot="header">
|
||||||
|
<div>
|
||||||
<span class="h4">Create Channel</span>
|
<span class="h4">Create Channel</span>
|
||||||
{#if community.id !== -1}
|
{#if community.id !== -1}
|
||||||
<span class="text-fg-3 text-small">in <span class="text-fg-2">{ community.name }</span></span>
|
<span class="text-fg-3 text-small">in <span class="text-fg-2">{ community.name }</span></span>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
@ -36,7 +39,6 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
|
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !createButtonEnabled }">Create</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore, userInfoStore } from "../../stores";
|
||||||
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
import { getGrantFor, CapabilityType } from "../../permissions";
|
||||||
|
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "../../request";
|
||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
||||||
|
import EditableText from "../EditableText.svelte";
|
||||||
|
import FetchedUserView from "../FetchedUserView.svelte";
|
||||||
|
|
||||||
export let channel;
|
export let channel;
|
||||||
|
export let place = null;
|
||||||
|
|
||||||
let channelName = channel.name;
|
|
||||||
let buttonsEnabled = true;
|
let buttonsEnabled = true;
|
||||||
let response;
|
let response;
|
||||||
|
$: grant = getGrantFor($userInfoStore, CapabilityType.ManageChannel, channel);
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async (newName) => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
response = await remoteSignal(methods.updateChannelName, channel.id, channelName);
|
response = await remoteSignal(methods.updateChannelName, channel.id, newName);
|
||||||
buttonsEnabled = true;
|
buttonsEnabled = true;
|
||||||
if (responseOk(response)) {
|
if (responseOk(response)) {
|
||||||
close();
|
channel.name = newName;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const deleteChannel = async () => {
|
const deleteChannel = async () => {
|
||||||
|
@ -30,26 +34,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-button {
|
.created-by {
|
||||||
color: var(--red-2);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal {close} enter={save}>
|
<Modal {close} {place} enter={save} showCloseButton={false}>
|
||||||
<span class="h4" slot="header">Edit Channel</span>
|
<svelte:fragment slot="header">
|
||||||
|
<EditableText slot="header" value="{ channel.name }" disabled={ !buttonsEnabled || !grant } onSave={ save }></EditableText>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
<RpcErrorDisplay response={response} />
|
<RpcErrorDisplay response={response} />
|
||||||
<RpcErrorDisplay validationIndex={1} response={response} />
|
<RpcErrorDisplay validationIndex={1} response={response} />
|
||||||
<label class="input-label">
|
|
||||||
<span>Channel Name</span>
|
|
||||||
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
|
|
||||||
</label>
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<div class="created-by">
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
<FetchedUserView userId={ channel.owner_id } capabilityType={ CapabilityType.ManageChannel } capabilityResource={ channel } />
|
||||||
<button class="button modal-secondary-action delete-button" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
|
</div>
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
|
||||||
</svelte:fragment>
|
{#if grant}
|
||||||
|
<button class="hyperlink-button button-danger" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore, userInfoStore } from "../../stores";
|
||||||
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
import { getGrantFor, CapabilityType } from "../../permissions";
|
||||||
|
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "../../request";
|
||||||
import Modal from "./Modal.svelte";
|
import Modal from "./Modal.svelte";
|
||||||
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
||||||
|
import EditableText from "../EditableText.svelte";
|
||||||
|
import FetchedUserView from "../FetchedUserView.svelte";
|
||||||
|
|
||||||
export let community;
|
export let community;
|
||||||
|
export let place = null;
|
||||||
|
|
||||||
let communityName = community.name;
|
|
||||||
let buttonsEnabled = true;
|
let buttonsEnabled = true;
|
||||||
let response;
|
let response;
|
||||||
|
$: grant = getGrantFor($userInfoStore, CapabilityType.ManageCommunity, community);
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async (newName) => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
response = await remoteSignal(methods.updateCommunityName, community.id, communityName);
|
response = await remoteSignal(methods.updateCommunityName, community.id, newName);
|
||||||
buttonsEnabled = true;
|
buttonsEnabled = true;
|
||||||
if (responseOk(response)) {
|
if (responseOk(response)) {
|
||||||
close();
|
community.name = newName;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const deleteCommunity = async () => {
|
const deleteCommunity = async () => {
|
||||||
|
@ -30,26 +34,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-button {
|
.created-by {
|
||||||
color: var(--red-2);
|
margin-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal {close} enter={save}>
|
<Modal {close} {place} enter={save} showCloseButton={false}>
|
||||||
<span class="h4" slot="header">Edit Community</span>
|
<svelte:fragment slot="header">
|
||||||
|
<EditableText slot="header" value="{ community.name }" disabled={ !buttonsEnabled || !grant } onSave={ save }></EditableText>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
<RpcErrorDisplay response={response} />
|
<RpcErrorDisplay response={response} />
|
||||||
<RpcErrorDisplay validationIndex={1} response={response} />
|
<RpcErrorDisplay validationIndex={1} response={response} />
|
||||||
<label class="input-label">
|
|
||||||
<span>Community Name</span>
|
|
||||||
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />
|
|
||||||
</label>
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<div class="created-by">
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
<FetchedUserView userId={ community.owner_id } capabilityType={ CapabilityType.ManageCommunity } capabilityResource={ community } />
|
||||||
<button class="button modal-secondary-action delete-button" on:click="{ deleteCommunity }" disabled="{ !buttonsEnabled }">Delete</button>
|
</div>
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
|
||||||
</svelte:fragment>
|
{#if grant}
|
||||||
|
<button class="hyperlink-button button-danger" on:click="{ deleteCommunity }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -28,12 +28,6 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.delete-button {
|
|
||||||
color: var(--red-2);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<Modal {close} enter={save}>
|
<Modal {close} enter={save}>
|
||||||
<span class="h4" slot="header">Edit Message</span>
|
<span class="h4" slot="header">Edit Message</span>
|
||||||
|
|
||||||
|
@ -47,8 +41,7 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
<button class="hyperlink-button padded button-danger" on:click="{ deleteMessage }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||||
<button class="button modal-secondary-action delete-button" on:click="{ deleteMessage }" disabled="{ !buttonsEnabled }">Delete</button>
|
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal opaque close={createAccountInstead} {outroEnd} enter={login}>
|
<Modal opaque {outroEnd} enter={login} showCloseButton={false}>
|
||||||
<span class="h4" slot="header">Welcome back!</span>
|
<span class="h4" slot="header">Welcome back!</span>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="button modal-secondary-action" on:click="{ createAccountInstead }" disabled="{ !buttonsEnabled }">Create an account instead</button>
|
<button class="hyperlink-button padded" on:click="{ createAccountInstead }" disabled="{ !buttonsEnabled }">Create an account instead</button>
|
||||||
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
|
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { maybeModalFade, maybeModalFadeIf, maybeModalScale } from "../../animations";
|
import { maybeModalFade, maybeModalFadeIf, maybeModalScale } from "../../animations";
|
||||||
|
import { OverlayType, overlayStore } from "../../stores";
|
||||||
|
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
export let enter = () => {};
|
export let enter = () => {};
|
||||||
export let outroEnd = () => {};
|
export let outroEnd = () => {};
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let opaque = false;
|
export let opaque = false;
|
||||||
|
export let place = null;
|
||||||
|
export let showCloseButton = true;
|
||||||
let modal;
|
let modal;
|
||||||
let blur = false;
|
let blur = false;
|
||||||
|
|
||||||
|
$: backdropStyle = !!place ? `top: ${place.y || 0}px; left: ${place.x || 0}px;` : "";
|
||||||
|
|
||||||
const onKeydown = ({ code }) => {
|
const onKeydown = ({ code }) => {
|
||||||
if (code === "Enter") {
|
if (code === "Enter") {
|
||||||
enter();
|
enter();
|
||||||
|
@ -31,14 +36,25 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.close-modal {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
<div class="modal-backdrop" class:modal-backdrop-opaque={opaque} class:blur={blur} transition:maybeModalFadeIf="{{ _condition: !opaque }}" on:click="{ close }" on:keydown="{ onKeydown }" on:introend={backdropIntroEnd} on:outrostart={backdropOutroStart}>
|
<div class="modal-backdrop" style="{backdropStyle}" class:positioned={!!place} class:modal-backdrop-opaque={opaque} class:blur={blur} transition:maybeModalFadeIf="{{ _condition: !opaque }}" on:click="{ close }" on:keydown="{ onKeydown }" on:introend={backdropIntroEnd} on:outrostart={backdropOutroStart}>
|
||||||
<div bind:this={modal} role="alertdialog" tabindex="-1" aria-modal="true" class={className + " modal"} transition:maybeModalScale on:click|stopPropagation on:outroend={outroEnd}>
|
<div bind:this={modal} class:positioned={!!place} role="alertdialog" tabindex="-1" aria-modal="true" class={className + " modal"} transition:maybeModalScale on:click|stopPropagation on:outroend={outroEnd}>
|
||||||
{#if $$slots.header}
|
{#if $$slots.header}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
|
{#if showCloseButton}
|
||||||
|
<button class="icon-button material-icons-outlined close-modal" on:click="{ close }" aria-label="Close">
|
||||||
|
close
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore } from "../../stores";
|
import { overlayStore, smallViewport } from "../../stores";
|
||||||
|
|
||||||
import EditChannel from "./EditChannel.svelte";
|
import EditChannel from "./EditChannel.svelte";
|
||||||
import CreateChannel from "./CreateChannel.svelte";
|
import CreateChannel from "./CreateChannel.svelte";
|
||||||
|
@ -9,7 +9,6 @@
|
||||||
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";
|
||||||
import UserInfo from "./UserInfo.svelte";
|
|
||||||
import AddCommunity from "./AddCommunity.svelte";
|
import AddCommunity from "./AddCommunity.svelte";
|
||||||
import EditCommunity from "./EditCommunity.svelte";
|
import EditCommunity from "./EditCommunity.svelte";
|
||||||
|
|
||||||
|
@ -22,12 +21,11 @@
|
||||||
5: EditMessage,
|
5: EditMessage,
|
||||||
6: Settings,
|
6: Settings,
|
||||||
7: Prompt,
|
7: Prompt,
|
||||||
8: UserInfo,
|
8: AddCommunity,
|
||||||
9: AddCommunity,
|
9: EditCommunity,
|
||||||
10: EditCommunity,
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each $overlayStore as overlay (overlay.id)}
|
{#each $overlayStore as overlay (overlay.id)}
|
||||||
<svelte:component this={ OverlayComponent[overlay.type] } {...overlay.props} />
|
<svelte:component this={ OverlayComponent[overlay.type] } {...overlay.props} place={ $smallViewport ? undefined : overlay.props.place } />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<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>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
<script>
|
|
||||||
import { maybeModalFade, maybeModalScale } from "../../animations";
|
|
||||||
|
|
||||||
export let presenceEntry;
|
|
||||||
export let close = () => {};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.user-info-modal {
|
|
||||||
max-width: 560px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info-row .material-icons-outlined {
|
|
||||||
margin-right: var(--space-xs);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }">
|
|
||||||
<div class="modal user-info-modal" transition:maybeModalScale on:click|stopPropagation>
|
|
||||||
<div class="modal-header">
|
|
||||||
<span class="h4">{ presenceEntry.user.username }</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-content">
|
|
||||||
{#if presenceEntry.bridgesTo}
|
|
||||||
<div class="user-info-row">
|
|
||||||
<span class="material-icons-outlined">cloud_sync</span>
|
|
||||||
<span>This application may send messages and metadata to <b>{presenceEntry.bridgesTo}</b></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if presenceEntry.privacy}
|
|
||||||
<div class="user-info-row">
|
|
||||||
<span class="material-icons-outlined">policy</span>
|
|
||||||
<span>Data accessible by this application is processed in accordance with their Privacy Policy: <b>{ presenceEntry.privacy }</b></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if presenceEntry.terms}
|
|
||||||
<div class="user-info-row">
|
|
||||||
<span class="material-icons-outlined">gavel</span>
|
|
||||||
<span>The Terms of Service of this application can be found at: <b>{ presenceEntry.terms }</b></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if presenceEntry.bridgesTo || presenceEntry.privacy || presenceEntry.terms}
|
|
||||||
<div class="user-info-row">
|
|
||||||
<span class="material-icons-outlined">shield</span>
|
|
||||||
<span>You may be able to opt out of the above</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
49
frontend/src/permissions.js
Normal file
49
frontend/src/permissions.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export const CapabilityGrant = {
|
||||||
|
None: 0,
|
||||||
|
ResourceOwner: 1,
|
||||||
|
ResourceManager: 2,
|
||||||
|
GlobalSuperuser: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CapabilityType = {
|
||||||
|
None: 0,
|
||||||
|
ManageChannel: 1,
|
||||||
|
ManageCommunity: 2,
|
||||||
|
ManageMessage: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function getGrantFor(user, capability, resource) {
|
||||||
|
let resourceOwnerId = -1;
|
||||||
|
|
||||||
|
switch (capability) {
|
||||||
|
case CapabilityType.ManageChannel: {
|
||||||
|
resourceOwnerId = resource.owner_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CapabilityType.ManageCommunity: {
|
||||||
|
resourceOwnerId = resource.owner_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CapabilityType.ManageMessage: {
|
||||||
|
resourceOwnerId = resource.author_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return CapabilityGrant.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user && user.id === resourceOwnerId) {
|
||||||
|
return CapabilityGrant.ResourceOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user && user.is_superuser) {
|
||||||
|
return CapabilityGrant.GlobalSuperuser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CapabilityGrant.None;
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ export const methods = {
|
||||||
getUserSelf: withCacheable(method(102, true)),
|
getUserSelf: withCacheable(method(102, true)),
|
||||||
promoteUserSelf: method(103, true),
|
promoteUserSelf: method(103, true),
|
||||||
putUserAvatar: method(104, true),
|
putUserAvatar: method(104, true),
|
||||||
|
getUser: withCacheable(method(105, true)),
|
||||||
createChannel: method(200, true),
|
createChannel: method(200, true),
|
||||||
updateChannelName: method(201, true),
|
updateChannelName: method(201, true),
|
||||||
deleteChannel: method(202, true),
|
deleteChannel: method(202, true),
|
||||||
|
@ -73,6 +74,10 @@ export function getErrorFromResponse(response) {
|
||||||
return { message: rpcErrorMessage, validationErrors: response.data.errors };
|
return { message: rpcErrorMessage, validationErrors: response.data.errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.data.code === RPCError.BAD_REQUEST.code) {
|
||||||
|
return { message: rpcErrorMessage + (response.data.detail ? `: ${response.data.detail}` : "") };
|
||||||
|
}
|
||||||
|
|
||||||
return { message: rpcErrorMessage };
|
return { message: rpcErrorMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getItem } from "./storage";
|
import { getItem } from "./storage";
|
||||||
import { showSidebar, smallViewport, theme, usesKeyboardNavigation } from "./stores";
|
import { showSidebar, smallViewport, theme, usesKeyboardNavigation, overlayStore } from "./stores";
|
||||||
|
|
||||||
function initViewportSizeHandler() {
|
function initViewportSizeHandler() {
|
||||||
const root = document.querySelector(':root');
|
const root = document.querySelector(':root');
|
||||||
|
@ -33,22 +33,30 @@ function updateTheme(themeName) {
|
||||||
classes.add(`theme--${themeName}`);
|
classes.add(`theme--${themeName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initKeyboardNavigationDetection() {
|
export function initResponsiveHandlers() {
|
||||||
|
// Keyboard navigation detection
|
||||||
document.addEventListener("keydown", ({ key }) => {
|
document.addEventListener("keydown", ({ key }) => {
|
||||||
if (key === "Tab") {
|
if (key === "Tab") {
|
||||||
usesKeyboardNavigation.set(true);
|
usesKeyboardNavigation.set(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", e => {
|
const keyboardClickHandler = (e) => {
|
||||||
// screenX and screenY are 0 when a user presses enter for navigation
|
// screenX and screenY are 0 when a user presses enter for navigation
|
||||||
usesKeyboardNavigation.set(!e.screenX && !e.screenY);
|
usesKeyboardNavigation.set(!e.screenX && !e.screenY);
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
const overlayClickHandler = () => {
|
||||||
|
overlayStore.closeAllAbsolute();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", e => {
|
||||||
|
keyboardClickHandler(e);
|
||||||
|
overlayClickHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export function initResponsiveHandlers() {
|
|
||||||
initViewportSizeHandler();
|
initViewportSizeHandler();
|
||||||
initKeyboardNavigationDetection();
|
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(min-width: 768px)');
|
const mediaQuery = window.matchMedia('(min-width: 768px)');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
|
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
|
||||||
import logger from "./logging";
|
import logger from "./logging";
|
||||||
|
import { CapabilityType, getGrantFor } from "./permissions";
|
||||||
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "./request";
|
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "./request";
|
||||||
import { getItem, setItem } from "./storage";
|
import { getItem, setItem } from "./storage";
|
||||||
|
|
||||||
|
@ -236,7 +237,7 @@ class MessageStore extends Store {
|
||||||
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
|
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
|
||||||
message._mentions = true;
|
message._mentions = true;
|
||||||
}
|
}
|
||||||
if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) {
|
if (getGrantFor(userInfoStore.value, CapabilityType.ManageMessage, message)) {
|
||||||
message._editable = true;
|
message._editable = true;
|
||||||
}
|
}
|
||||||
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) {
|
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) {
|
||||||
|
@ -400,19 +401,39 @@ export const OverlayType = {
|
||||||
EditMessage: 5,
|
EditMessage: 5,
|
||||||
Settings: 6,
|
Settings: 6,
|
||||||
Prompt: 7,
|
Prompt: 7,
|
||||||
UserInfo: 8,
|
AddCommunity: 8,
|
||||||
AddCommunity: 9,
|
EditCommunity: 9
|
||||||
EditCommunity: 10
|
|
||||||
};
|
};
|
||||||
class OverlayStore extends Store {
|
class OverlayStore extends Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
super([], "OverlayStore");
|
super([], "OverlayStore");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeAllAbsolute() {
|
||||||
|
const toRemove = [];
|
||||||
|
for (let i = 0; i < this.value.length; i++) {
|
||||||
|
const e = this.value[i];
|
||||||
|
if (e.props && e.props.place) {
|
||||||
|
toRemove.push(e.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toRemove.forEach(id => this.popId(id));
|
||||||
|
}
|
||||||
|
|
||||||
isOverlayPresent() {
|
isOverlayPresent() {
|
||||||
return !!this.value.length;
|
return !!this.value.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushAbsolute(type, x=0, y=0, props={}) {
|
||||||
|
this.closeAllAbsolute();
|
||||||
|
return this.push(type, {
|
||||||
|
...props,
|
||||||
|
place: {
|
||||||
|
x, y
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
push(type, props={}) {
|
push(type, props={}) {
|
||||||
const id = Math.floor(Math.random() * 9999999);
|
const id = Math.floor(Math.random() * 9999999);
|
||||||
|
|
||||||
|
@ -581,9 +602,6 @@ class PresenceStore extends Store {
|
||||||
// don't need to push the status, since we remove offline members from the presence list
|
// don't need to push the status, since we remove offline members from the presence list
|
||||||
this.value.push({
|
this.value.push({
|
||||||
user: entry.user,
|
user: entry.user,
|
||||||
bridgesTo: entry.bridgesTo,
|
|
||||||
privacy: entry.privacy,
|
|
||||||
terms: entry.terms
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -178,6 +178,15 @@ body {
|
||||||
backdrop-filter: blur(1.5px);
|
backdrop-filter: blur(1.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.positioned {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: unset;
|
||||||
|
contain: content;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-backdrop-opaque {
|
.modal-backdrop-opaque {
|
||||||
background-color: var(--background-color-1);
|
background-color: var(--background-color-1);
|
||||||
backdrop-filter: unset;
|
backdrop-filter: unset;
|
||||||
|
@ -193,6 +202,8 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
}
|
}
|
||||||
|
@ -203,25 +214,38 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: var(--space-norm);
|
padding: var(--space-norm);
|
||||||
background-color: var(--background-color-1);
|
padding-top: var(--space-xs);
|
||||||
|
background-color: transparent;
|
||||||
border-bottom-right-radius: var(--radius-lg);
|
border-bottom-right-radius: var(--radius-lg);
|
||||||
border-bottom-left-radius: var(--radius-lg);
|
border-bottom-left-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal.positioned {
|
||||||
|
background-color: hsla(0, 0%, 8%, 85%);
|
||||||
|
backdrop-filter: blur(1.5px);
|
||||||
|
width: 325px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme--light .modal.positioned {
|
||||||
|
background-color: var(--background-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.positioned .modal-footer {
|
||||||
|
padding-top: var(--space-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-primary-action {
|
.modal-primary-action {
|
||||||
float: right;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-secondary-action {
|
.modal-secondary-action {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-backdrop-opaque .modal .modal-footer {
|
|
||||||
background-color: var(--background-color-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.modal {
|
.modal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -272,6 +296,27 @@ body {
|
||||||
|
|
||||||
/* button */
|
/* button */
|
||||||
|
|
||||||
|
.hyperlink-button {
|
||||||
|
color: var(--foreground-color-1);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--h6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hyperlink-button.padded {
|
||||||
|
padding: 0.85em;
|
||||||
|
padding-top: 0.65em;
|
||||||
|
padding-bottom: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hyperlink-button:hover {
|
||||||
|
color: var(--foreground-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
color: var(--foreground-color-1);
|
color: var(--foreground-color-1);
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -283,7 +328,7 @@ body {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-weight: 550;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
|
@ -292,34 +337,25 @@ body {
|
||||||
|
|
||||||
.button-accent {
|
.button-accent {
|
||||||
color: var(--colored-element-text-color);
|
color: var(--colored-element-text-color);
|
||||||
background-color: var(--purple-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-accent:hover {
|
|
||||||
background-color: var(--purple-1);
|
background-color: var(--purple-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-accent:disabled {
|
.button-accent:hover {
|
||||||
background-color: var(--purple-2);
|
background-color: var(--purple-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-red {
|
.button-accent:disabled {
|
||||||
color: var(--colored-element-text-color);
|
background-color: var(--purple-1);
|
||||||
background-color: var(--red-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-red:hover {
|
|
||||||
background-color: var(--red-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-red:disabled {
|
|
||||||
background-color: var(--red-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-danger {
|
.button-danger {
|
||||||
color: var(--red-2);
|
color: var(--red-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-danger:hover {
|
||||||
|
color: var(--red-1);
|
||||||
|
}
|
||||||
|
|
||||||
/* icon buttons */
|
/* icon buttons */
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
|
@ -531,7 +567,11 @@ body {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: x-small;
|
font-size: x-small;
|
||||||
margin-left: var(--space-sm);
|
margin-left: var(--space-sm);
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
|
.user-badge.secondary {
|
||||||
|
background-color: var(--background-color-1);
|
||||||
|
border: 1px solid var(--background-color-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* util */
|
/* util */
|
||||||
|
|
|
@ -167,9 +167,6 @@ export class GatewayClient {
|
||||||
lastAliveCheck: number;
|
lastAliveCheck: number;
|
||||||
clientDispatchChannels: Set<string>;
|
clientDispatchChannels: Set<string>;
|
||||||
messagesSinceLastCheck: number;
|
messagesSinceLastCheck: number;
|
||||||
bridgesTo?: string;
|
|
||||||
privacy?: string;
|
|
||||||
terms?: string;
|
|
||||||
|
|
||||||
constructor(ws: WebSocket) {
|
constructor(ws: WebSocket) {
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
@ -179,9 +176,6 @@ export class GatewayClient {
|
||||||
this.lastAliveCheck = performance.now();
|
this.lastAliveCheck = performance.now();
|
||||||
this.clientDispatchChannels = new Set();
|
this.clientDispatchChannels = new Set();
|
||||||
this.messagesSinceLastCheck = 0;
|
this.messagesSinceLastCheck = 0;
|
||||||
this.bridgesTo = undefined;
|
|
||||||
this.privacy = undefined;
|
|
||||||
this.terms = undefined;
|
|
||||||
|
|
||||||
gatewayClients.add(this);
|
gatewayClients.add(this);
|
||||||
this.ws.on("close", this.handleClose.bind(this));
|
this.ws.on("close", this.handleClose.bind(this));
|
||||||
|
@ -243,9 +237,6 @@ export class GatewayClient {
|
||||||
avatar: this.user.avatar
|
avatar: this.user.avatar
|
||||||
},
|
},
|
||||||
status,
|
status,
|
||||||
bridgesTo: this.bridgesTo,
|
|
||||||
privacy: this.privacy,
|
|
||||||
terms: this.terms,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { errors } from "../../errors";
|
||||||
import { query } from "../../database";
|
import { query } from "../../database";
|
||||||
import { compare, hash } from "bcrypt";
|
import { compare, hash } from "bcrypt";
|
||||||
import { getPublicUserObject, loginAttempt } from "../../auth";
|
import { getPublicUserObject, loginAttempt } from "../../auth";
|
||||||
import { RPCContext, bufferSlice, method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc";
|
import { RPCContext, bufferSlice, method, methodButWarningDoesNotAuthenticate, string, uint, usernameRegex, withRegexp } from "./../rpc";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { unlink } from "fs/promises";
|
import { unlink } from "fs/promises";
|
||||||
|
@ -165,3 +165,19 @@ method(
|
||||||
return filenames;
|
return filenames;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getUser",
|
||||||
|
[uint("id", "ID of the user to get")],
|
||||||
|
async (_: User, id: number) => {
|
||||||
|
const getUserResult = await query("SELECT * FROM users WHERE id = $1", [id]);
|
||||||
|
if (!getUserResult) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
if (getUserResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPublicUserObject(getUserResult.rows[0]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
3
src/types/gatewaypresence.d.ts
vendored
3
src/types/gatewaypresence.d.ts
vendored
|
@ -6,8 +6,5 @@ export interface GatewayPresenceEntry {
|
||||||
id: number,
|
id: number,
|
||||||
avatar: string | null
|
avatar: string | null
|
||||||
},
|
},
|
||||||
bridgesTo?: string,
|
|
||||||
privacy?: string,
|
|
||||||
terms?: string,
|
|
||||||
status: GatewayPresenceStatus
|
status: GatewayPresenceStatus
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue