Compare commits
No commits in common. "b100f13640933cbbcaff37c370b58f9045956d0a" and "b1a4622151f7cf90f809e36cffc5e1ac93abe21b" have entirely different histories.
b100f13640
...
b1a4622151
29 changed files with 214 additions and 390 deletions
|
@ -11,7 +11,11 @@ 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|stopPropagation="{ ({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.EditChannel, pageX, pageY, {channel}) }">{ channel.name }</span>
|
<span class="text-small top-bar-heading accent" 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">
|
||||||
<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, event) => {
|
const optionClick = (option) => {
|
||||||
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(event);
|
option.handle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -45,12 +45,6 @@
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -71,13 +65,17 @@
|
||||||
.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|stopPropagation="{ e => optionClick(option, e) }">
|
<button class="button" class:selected={ selectedOptionId === option.id } class:has-text={!!option.text} on:click={ optionClick(option) }>
|
||||||
{#if option.icon}
|
{#if option.icon}
|
||||||
<span class="material-icons-outlined">{ option.icon }</span>
|
<span class="material-icons-outlined">{ option.icon }</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<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,14 +92,12 @@
|
||||||
<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) }" width=400 height=400></video>
|
<video controls="controls" class="attachment media" src="{ attachmentUrl(attachment.file) }"></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,10 +23,6 @@
|
||||||
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 }}">
|
||||||
|
@ -37,6 +33,9 @@
|
||||||
{#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|stopPropagation={({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.AddCommunity, pageX, pageY)}>
|
<button class="button" on:click={() => overlayStore.push(OverlayType.AddCommunity)}>
|
||||||
<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,9 +109,11 @@
|
||||||
{#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}
|
||||||
<button class="icon-button" on:click|stopPropagation="{ ({ pageX, pageY }) => overlayStore.pushAbsolute(OverlayType.EditChannel, pageX, pageY, { channel }) }" aria-label="Edit Channel">
|
{#if $userInfoStore && (channel.owner_id === $userInfoStore.id || $userInfoStore.is_superuser)}
|
||||||
<span class="material-icons-outlined">more_vert</span>
|
<button class="icon-button" on:click|stopPropagation="{ () => overlayStore.push(OverlayType.EditChannel, { channel }) }" aria-label="Edit Channel">
|
||||||
</button>
|
<span class="material-icons-outlined">more_vert</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<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";
|
||||||
|
@ -18,18 +17,18 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "EDIT_COMMUNITY",
|
id: "EDIT_COMMUNITY",
|
||||||
icon: "more_vert",
|
icon: "edit",
|
||||||
hidden: isUser || !communityLike,
|
hidden: !(!isUser && communityLike && $userInfoStore && communityLike.owner_id === $userInfoStore.id),
|
||||||
handle({ pageX, pageY }) {
|
handle() {
|
||||||
overlayStore.pushAbsolute(OverlayType.EditCommunity, pageX, pageY, { community: communityLike });
|
overlayStore.push(OverlayType.EditCommunity, { 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({ pageX, pageY }) {
|
handle() {
|
||||||
overlayStore.pushAbsolute(OverlayType.CreateChannel, pageX, pageY, { community: isUser ? { id: -1 } : communityLike });
|
overlayStore.push(OverlayType.CreateChannel, { community: isUser ? { id: -1 } : communityLike });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
<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;
|
||||||
font-size: var(--h6);
|
justify-content: center;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -34,19 +30,9 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if user && user.avatar}
|
{#if user && user.avatar}
|
||||||
<img src={avatarUrl(user.avatar, size)} width={size} height={size} alt=" ">
|
<img src={avatarUrl(user.avatar, size)} alt=" ">
|
||||||
{:else}
|
{:else}
|
||||||
<span class="material-icons-outlined circled-icon" style="width: {size}px; height: {size}px;">alternate_email</span>
|
<span class="material-icons-outlined circled-icon">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,7 +7,6 @@
|
||||||
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;
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {place} {close} enter={create} showCloseButton={false}>
|
<Modal {close} enter={create}>
|
||||||
<span class="h4" slot="header">Create Community</span>
|
<span class="h4" slot="header">Create Community</span>
|
||||||
|
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
@ -31,6 +30,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="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 {outroEnd} enter={create} showCloseButton={false}>
|
<Modal opaque close={loginInstead} {outroEnd} enter={create}>
|
||||||
<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="hyperlink-button padded" on:click="{ loginInstead }" disabled="{ !buttonsEnabled }">Log in instead</button>
|
<button class="button modal-secondary-action" 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,7 +8,6 @@
|
||||||
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;
|
||||||
|
@ -19,14 +18,12 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {place} {close} enter={create} showCloseButton={false}>
|
<Modal {close} enter={create}>
|
||||||
<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">
|
||||||
|
@ -39,6 +36,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="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,26 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore, userInfoStore } from "../../stores";
|
import { overlayStore } from "../../stores";
|
||||||
import { getGrantFor, CapabilityType } from "../../permissions";
|
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
||||||
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 (newName) => {
|
const save = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
response = await remoteSignal(methods.updateChannelName, channel.id, newName);
|
response = await remoteSignal(methods.updateChannelName, channel.id, channelName);
|
||||||
buttonsEnabled = true;
|
buttonsEnabled = true;
|
||||||
if (responseOk(response)) {
|
if (responseOk(response)) {
|
||||||
channel.name = newName;
|
close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const deleteChannel = async () => {
|
const deleteChannel = async () => {
|
||||||
|
@ -34,26 +30,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.created-by {
|
.delete-button {
|
||||||
margin-bottom: var(--space-md);
|
color: var(--red-2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal {close} {place} enter={save} showCloseButton={false}>
|
<Modal {close} enter={save}>
|
||||||
<svelte:fragment slot="header">
|
<span class="h4" slot="header">Edit Channel</span>
|
||||||
<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">
|
||||||
<div class="created-by">
|
<span>Channel Name</span>
|
||||||
<FetchedUserView userId={ channel.owner_id } capabilityType={ CapabilityType.ManageChannel } capabilityResource={ channel } />
|
<input class="input full-width" minlength="1" maxlength="32" bind:value={ channelName } />
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{#if grant}
|
|
||||||
<button class="hyperlink-button button-danger" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
|
|
||||||
{/if}
|
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
||||||
|
<button class="button modal-secondary-action delete-button" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||||
|
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
||||||
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { overlayStore, userInfoStore } from "../../stores";
|
import { overlayStore } from "../../stores";
|
||||||
import { getGrantFor, CapabilityType } from "../../permissions";
|
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
||||||
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 (newName) => {
|
const save = async () => {
|
||||||
buttonsEnabled = false;
|
buttonsEnabled = false;
|
||||||
response = await remoteSignal(methods.updateCommunityName, community.id, newName);
|
response = await remoteSignal(methods.updateCommunityName, community.id, communityName);
|
||||||
buttonsEnabled = true;
|
buttonsEnabled = true;
|
||||||
if (responseOk(response)) {
|
if (responseOk(response)) {
|
||||||
community.name = newName;
|
close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const deleteCommunity = async () => {
|
const deleteCommunity = async () => {
|
||||||
|
@ -34,26 +30,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.created-by {
|
.delete-button {
|
||||||
margin-bottom: var(--space-md);
|
color: var(--red-2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Modal {close} {place} enter={save} showCloseButton={false}>
|
<Modal {close} enter={save}>
|
||||||
<svelte:fragment slot="header">
|
<span class="h4" slot="header">Edit Community</span>
|
||||||
<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">
|
||||||
<div class="created-by">
|
<span>Community Name</span>
|
||||||
<FetchedUserView userId={ community.owner_id } capabilityType={ CapabilityType.ManageCommunity } capabilityResource={ community } />
|
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{#if grant}
|
|
||||||
<button class="hyperlink-button button-danger" on:click="{ deleteCommunity }" disabled="{ !buttonsEnabled }">Delete</button>
|
|
||||||
{/if}
|
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
|
||||||
|
<button class="button modal-secondary-action delete-button" on:click="{ deleteCommunity }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||||
|
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Save</button>
|
||||||
|
</svelte:fragment>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -28,6 +28,12 @@
|
||||||
};
|
};
|
||||||
</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>
|
||||||
|
|
||||||
|
@ -41,7 +47,8 @@
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button class="hyperlink-button padded button-danger" on:click="{ deleteMessage }" disabled="{ !buttonsEnabled }">Delete</button>
|
<button class="button modal-secondary-action" on:click="{ close }">Cancel</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 {outroEnd} enter={login} showCloseButton={false}>
|
<Modal opaque close={createAccountInstead} {outroEnd} enter={login}>
|
||||||
<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="hyperlink-button padded" on:click="{ createAccountInstead }" disabled="{ !buttonsEnabled }">Create an account instead</button>
|
<button class="button modal-secondary-action" 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,19 +1,14 @@
|
||||||
<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") {
|
||||||
|
@ -36,25 +31,14 @@
|
||||||
});
|
});
|
||||||
</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" 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 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 bind:this={modal} class:positioned={!!place} role="alertdialog" tabindex="-1" aria-modal="true" class={className + " modal"} transition:maybeModalScale on:click|stopPropagation on:outroend={outroEnd}>
|
<div bind:this={modal} 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, smallViewport } 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";
|
||||||
|
@ -9,6 +9,7 @@
|
||||||
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";
|
||||||
|
|
||||||
|
@ -21,11 +22,12 @@
|
||||||
5: EditMessage,
|
5: EditMessage,
|
||||||
6: Settings,
|
6: Settings,
|
||||||
7: Prompt,
|
7: Prompt,
|
||||||
8: AddCommunity,
|
8: UserInfo,
|
||||||
9: EditCommunity,
|
9: AddCommunity,
|
||||||
|
10: EditCommunity,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each $overlayStore as overlay (overlay.id)}
|
{#each $overlayStore as overlay (overlay.id)}
|
||||||
<svelte:component this={ OverlayComponent[overlay.type] } {...overlay.props} place={ $smallViewport ? undefined : overlay.props.place } />
|
<svelte:component this={ OverlayComponent[overlay.type] } {...overlay.props} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
</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>
|
||||||
|
|
61
frontend/src/components/overlays/UserInfo.svelte
Normal file
61
frontend/src/components/overlays/UserInfo.svelte
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<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>
|
|
@ -1,49 +0,0 @@
|
||||||
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,7 +10,6 @@ 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),
|
||||||
|
@ -74,10 +73,6 @@ 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, overlayStore } from "./stores";
|
import { showSidebar, smallViewport, theme, usesKeyboardNavigation } from "./stores";
|
||||||
|
|
||||||
function initViewportSizeHandler() {
|
function initViewportSizeHandler() {
|
||||||
const root = document.querySelector(':root');
|
const root = document.querySelector(':root');
|
||||||
|
@ -33,30 +33,22 @@ function updateTheme(themeName) {
|
||||||
classes.add(`theme--${themeName}`);
|
classes.add(`theme--${themeName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initResponsiveHandlers() {
|
function initKeyboardNavigationDetection() {
|
||||||
// Keyboard navigation detection
|
|
||||||
document.addEventListener("keydown", ({ key }) => {
|
document.addEventListener("keydown", ({ key }) => {
|
||||||
if (key === "Tab") {
|
if (key === "Tab") {
|
||||||
usesKeyboardNavigation.set(true);
|
usesKeyboardNavigation.set(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyboardClickHandler = (e) => {
|
document.addEventListener("click", 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,6 +1,5 @@
|
||||||
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";
|
||||||
|
|
||||||
|
@ -237,7 +236,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 (getGrantFor(userInfoStore.value, CapabilityType.ManageMessage, message)) {
|
if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) {
|
||||||
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) {
|
||||||
|
@ -401,39 +400,19 @@ export const OverlayType = {
|
||||||
EditMessage: 5,
|
EditMessage: 5,
|
||||||
Settings: 6,
|
Settings: 6,
|
||||||
Prompt: 7,
|
Prompt: 7,
|
||||||
AddCommunity: 8,
|
UserInfo: 8,
|
||||||
EditCommunity: 9
|
AddCommunity: 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);
|
||||||
|
|
||||||
|
@ -602,6 +581,9 @@ 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,15 +178,6 @@ 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;
|
||||||
|
@ -202,8 +193,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
}
|
}
|
||||||
|
@ -214,38 +203,25 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: var(--space-norm);
|
padding: var(--space-norm);
|
||||||
padding-top: var(--space-xs);
|
background-color: var(--background-color-1);
|
||||||
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 {
|
||||||
margin-left: auto;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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%;
|
||||||
|
@ -296,27 +272,6 @@ 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;
|
||||||
|
@ -328,7 +283,7 @@ body {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-weight: 600;
|
font-weight: 550;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
|
@ -337,25 +292,34 @@ body {
|
||||||
|
|
||||||
.button-accent {
|
.button-accent {
|
||||||
color: var(--colored-element-text-color);
|
color: var(--colored-element-text-color);
|
||||||
background-color: var(--purple-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-accent:hover {
|
|
||||||
background-color: var(--purple-2);
|
background-color: var(--purple-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-accent:disabled {
|
.button-accent:hover {
|
||||||
background-color: var(--purple-1);
|
background-color: var(--purple-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-accent:disabled {
|
||||||
|
background-color: var(--purple-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-red {
|
||||||
|
color: var(--colored-element-text-color);
|
||||||
|
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 {
|
||||||
|
@ -567,11 +531,7 @@ 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,6 +167,9 @@ 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;
|
||||||
|
@ -176,6 +179,9 @@ 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));
|
||||||
|
@ -237,6 +243,9 @@ 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, uint, usernameRegex, withRegexp } from "./../rpc";
|
import { RPCContext, bufferSlice, method, methodButWarningDoesNotAuthenticate, string, 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,19 +165,3 @@ 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,5 +6,8 @@ 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