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
|
||||
}
|
||||
);
|
||||
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.discordChannelIdToWaffleChannelIdMap = new Map();
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<span class="unread-indicator">{$totalUnreadsStore}</span>
|
||||
{/if}
|
||||
<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">
|
||||
<button class="icon-button" on:click="{ () => showPresenceSidebar.set(!showPresenceSidebar.value) }" aria-label="Toggle user list">
|
||||
<span class="material-icons-outlined">people</span>
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
onSelect(selectedOptionId);
|
||||
}
|
||||
|
||||
const optionClick = (option, event) => {
|
||||
const optionClick = (option) => {
|
||||
if (option) {
|
||||
if (doHighlight) {
|
||||
selectedOptionId = option.id;
|
||||
}
|
||||
onSelect(selectedOptionId);
|
||||
if (option.handle) {
|
||||
option.handle(event);
|
||||
option.handle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -45,12 +45,6 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
.smaller button {
|
||||
font-size: 0.85em;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--background-color-3);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--background-color-3);
|
||||
}
|
||||
|
@ -71,13 +65,17 @@
|
|||
.has-text .material-icons-outlined {
|
||||
margin-right: var(--space-xxs);
|
||||
}
|
||||
|
||||
.smaller button {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<div class:smaller={smaller}>
|
||||
{#each options as option (option.id)}
|
||||
{#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}
|
||||
<span class="material-icons-outlined">{ option.icon }</span>
|
||||
{/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) }">
|
||||
{:else if renderAs === AttachmentRenderAs.Video}
|
||||
<!-- 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}
|
||||
<div class="attachment attachment-card">
|
||||
<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>
|
||||
</div>
|
||||
{:else if renderAs === AttachmentRenderAs.Audio}
|
||||
<audio controls="controls" class="attachment media" src="{ attachmentUrl(attachment.file) }"></audio>
|
||||
{:else}
|
||||
<div class="attachment attachment-card">Couldn't render attachment</div>
|
||||
{/if}
|
||||
|
|
|
@ -23,10 +23,6 @@
|
|||
min-width: 248px;
|
||||
max-width: 248px;
|
||||
}
|
||||
|
||||
.sidebar-button {
|
||||
color: var(--foreground-color-2);
|
||||
}
|
||||
</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 }}">
|
||||
|
@ -37,6 +33,9 @@
|
|||
{#each $presenceStore as entry (entry.user.id)}
|
||||
<button class="sidebar-button">
|
||||
<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>
|
||||
{/each}
|
||||
{#if $smallViewport}
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
{/if}
|
||||
</button>
|
||||
{/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>
|
||||
</button>
|
||||
<button class="button" on:click={() => overlayStore.push(OverlayType.Settings)}>
|
||||
|
@ -109,9 +109,11 @@
|
|||
{#if $unreadStore.get(channel.id)}
|
||||
<div class="unread-indicator">{ $unreadStore.get(channel.id) }</div>
|
||||
{/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)}
|
||||
<button class="icon-button" on:click|stopPropagation="{ () => overlayStore.push(OverlayType.EditChannel, { channel }) }" aria-label="Edit Channel">
|
||||
<span class="material-icons-outlined">more_vert</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { CapabilityType, getGrantFor } from "../permissions";
|
||||
import { avatarUrl } from "../storage";
|
||||
import { OverlayType, overlayStore, userInfoStore } from "../stores";
|
||||
import ChipBar from "./ChipBar.svelte";
|
||||
|
@ -18,18 +17,18 @@
|
|||
},
|
||||
{
|
||||
id: "EDIT_COMMUNITY",
|
||||
icon: "more_vert",
|
||||
hidden: isUser || !communityLike,
|
||||
handle({ pageX, pageY }) {
|
||||
overlayStore.pushAbsolute(OverlayType.EditCommunity, pageX, pageY, { community: communityLike });
|
||||
icon: "edit",
|
||||
hidden: !(!isUser && communityLike && $userInfoStore && communityLike.owner_id === $userInfoStore.id),
|
||||
handle() {
|
||||
overlayStore.push(OverlayType.EditCommunity, { community: communityLike });
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "NEW_CHANNEL",
|
||||
icon: "add",
|
||||
hidden: (!$userInfoStore || !$userInfoStore.permissions.create_channel),
|
||||
handle({ pageX, pageY }) {
|
||||
overlayStore.pushAbsolute(OverlayType.CreateChannel, pageX, pageY, { community: isUser ? { id: -1 } : communityLike });
|
||||
handle() {
|
||||
overlayStore.push(OverlayType.CreateChannel, { community: isUser ? { id: -1 } : communityLike });
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<script>
|
||||
import { CapabilityGrant } from "../permissions";
|
||||
import { avatarUrl } from "../storage";
|
||||
|
||||
|
||||
export let user = null;
|
||||
export let size = 32;
|
||||
export let showBadges = true;
|
||||
export let grant = CapabilityGrant.None;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--h6);
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -34,19 +30,9 @@
|
|||
|
||||
<div>
|
||||
{#if user && user.avatar}
|
||||
<img src={avatarUrl(user.avatar, size)} width={size} height={size} alt=" ">
|
||||
<img src={avatarUrl(user.avatar, size)} alt=" ">
|
||||
{: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}
|
||||
<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>
|
|
@ -7,7 +7,6 @@
|
|||
let createButtonEnabled = true;
|
||||
let response;
|
||||
export let close = () => {};
|
||||
export let place = null;
|
||||
|
||||
const create = async () => {
|
||||
createButtonEnabled = false;
|
||||
|
@ -18,7 +17,7 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<Modal {place} {close} enter={create} showCloseButton={false}>
|
||||
<Modal {close} enter={create}>
|
||||
<span class="h4" slot="header">Create Community</span>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -31,6 +30,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<Modal opaque {outroEnd} enter={create} showCloseButton={false}>
|
||||
<Modal opaque close={loginInstead} {outroEnd} enter={create}>
|
||||
<span class="h4" slot="header">Create an Account</span>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -60,7 +60,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
let response;
|
||||
export let close = () => {};
|
||||
export let community = null;
|
||||
export let place = null;
|
||||
|
||||
const create = async () => {
|
||||
createButtonEnabled = false;
|
||||
|
@ -19,14 +18,12 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<Modal {place} {close} enter={create} showCloseButton={false}>
|
||||
<Modal {close} enter={create}>
|
||||
<svelte:fragment slot="header">
|
||||
<div>
|
||||
<span class="h4">Create Channel</span>
|
||||
{#if community.id !== -1}
|
||||
<span class="text-fg-3 text-small">in <span class="text-fg-2">{ community.name }</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -39,6 +36,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
<script>
|
||||
import { overlayStore, userInfoStore } from "../../stores";
|
||||
import { getGrantFor, CapabilityType } from "../../permissions";
|
||||
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "../../request";
|
||||
import { overlayStore } from "../../stores";
|
||||
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
||||
import Modal from "./Modal.svelte";
|
||||
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
||||
import EditableText from "../EditableText.svelte";
|
||||
import FetchedUserView from "../FetchedUserView.svelte";
|
||||
|
||||
export let channel;
|
||||
export let place = null;
|
||||
|
||||
let channelName = channel.name;
|
||||
let buttonsEnabled = true;
|
||||
let response;
|
||||
$: grant = getGrantFor($userInfoStore, CapabilityType.ManageChannel, channel);
|
||||
export let close = () => {};
|
||||
|
||||
const save = async (newName) => {
|
||||
const save = async () => {
|
||||
buttonsEnabled = false;
|
||||
response = await remoteSignal(methods.updateChannelName, channel.id, newName);
|
||||
response = await remoteSignal(methods.updateChannelName, channel.id, channelName);
|
||||
buttonsEnabled = true;
|
||||
if (responseOk(response)) {
|
||||
channel.name = newName;
|
||||
close();
|
||||
}
|
||||
};
|
||||
const deleteChannel = async () => {
|
||||
|
@ -34,26 +30,26 @@
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.created-by {
|
||||
margin-bottom: var(--space-md);
|
||||
.delete-button {
|
||||
color: var(--red-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal {close} {place} enter={save} showCloseButton={false}>
|
||||
<svelte:fragment slot="header">
|
||||
<EditableText slot="header" value="{ channel.name }" disabled={ !buttonsEnabled || !grant } onSave={ save }></EditableText>
|
||||
</svelte:fragment>
|
||||
<Modal {close} enter={save}>
|
||||
<span class="h4" slot="header">Edit Channel</span>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<RpcErrorDisplay response={response} />
|
||||
<RpcErrorDisplay validationIndex={1} response={response} />
|
||||
|
||||
<div class="created-by">
|
||||
<FetchedUserView userId={ channel.owner_id } capabilityType={ CapabilityType.ManageChannel } capabilityResource={ channel } />
|
||||
</div>
|
||||
|
||||
{#if grant}
|
||||
<button class="hyperlink-button button-danger" on:click="{ deleteChannel }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||
{/if}
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
<script>
|
||||
import { overlayStore, userInfoStore } from "../../stores";
|
||||
import { getGrantFor, CapabilityType } from "../../permissions";
|
||||
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "../../request";
|
||||
import { overlayStore } from "../../stores";
|
||||
import { getMessageFromResponse, methods, remoteSignal, responseOk } from "../../request";
|
||||
import Modal from "./Modal.svelte";
|
||||
import RpcErrorDisplay from "../rpc/RpcErrorDisplay.svelte";
|
||||
import EditableText from "../EditableText.svelte";
|
||||
import FetchedUserView from "../FetchedUserView.svelte";
|
||||
|
||||
export let community;
|
||||
export let place = null;
|
||||
|
||||
let communityName = community.name;
|
||||
let buttonsEnabled = true;
|
||||
let response;
|
||||
$: grant = getGrantFor($userInfoStore, CapabilityType.ManageCommunity, community);
|
||||
export let close = () => {};
|
||||
|
||||
const save = async (newName) => {
|
||||
const save = async () => {
|
||||
buttonsEnabled = false;
|
||||
response = await remoteSignal(methods.updateCommunityName, community.id, newName);
|
||||
response = await remoteSignal(methods.updateCommunityName, community.id, communityName);
|
||||
buttonsEnabled = true;
|
||||
if (responseOk(response)) {
|
||||
community.name = newName;
|
||||
close();
|
||||
}
|
||||
};
|
||||
const deleteCommunity = async () => {
|
||||
|
@ -34,26 +30,26 @@
|
|||
</script>
|
||||
|
||||
<style>
|
||||
.created-by {
|
||||
margin-bottom: var(--space-md);
|
||||
.delete-button {
|
||||
color: var(--red-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal {close} {place} enter={save} showCloseButton={false}>
|
||||
<svelte:fragment slot="header">
|
||||
<EditableText slot="header" value="{ community.name }" disabled={ !buttonsEnabled || !grant } onSave={ save }></EditableText>
|
||||
</svelte:fragment>
|
||||
<Modal {close} enter={save}>
|
||||
<span class="h4" slot="header">Edit Community</span>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<RpcErrorDisplay response={response} />
|
||||
<RpcErrorDisplay validationIndex={1} response={response} />
|
||||
|
||||
<div class="created-by">
|
||||
<FetchedUserView userId={ community.owner_id } capabilityType={ CapabilityType.ManageCommunity } capabilityResource={ community } />
|
||||
</div>
|
||||
|
||||
{#if grant}
|
||||
<button class="hyperlink-button button-danger" on:click="{ deleteCommunity }" disabled="{ !buttonsEnabled }">Delete</button>
|
||||
{/if}
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
@ -28,6 +28,12 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.delete-button {
|
||||
color: var(--red-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal {close} enter={save}>
|
||||
<span class="h4" slot="header">Edit Message</span>
|
||||
|
||||
|
@ -41,7 +47,8 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<Modal opaque {outroEnd} enter={login} showCloseButton={false}>
|
||||
<Modal opaque close={createAccountInstead} {outroEnd} enter={login}>
|
||||
<span class="h4" slot="header">Welcome back!</span>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
|
@ -61,7 +61,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { maybeModalFade, maybeModalFadeIf, maybeModalScale } from "../../animations";
|
||||
import { OverlayType, overlayStore } from "../../stores";
|
||||
|
||||
export let close = () => {};
|
||||
export let enter = () => {};
|
||||
export let outroEnd = () => {};
|
||||
export let className = "";
|
||||
export let opaque = false;
|
||||
export let place = null;
|
||||
export let showCloseButton = true;
|
||||
let modal;
|
||||
let blur = false;
|
||||
|
||||
$: backdropStyle = !!place ? `top: ${place.y || 0}px; left: ${place.x || 0}px;` : "";
|
||||
|
||||
const onKeydown = ({ code }) => {
|
||||
if (code === "Enter") {
|
||||
enter();
|
||||
|
@ -36,25 +31,14 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.close-modal {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-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 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 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} role="alertdialog" tabindex="-1" aria-modal="true" class={className + " modal"} transition:maybeModalScale on:click|stopPropagation on:outroend={outroEnd}>
|
||||
{#if $$slots.header}
|
||||
<div class="modal-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>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { overlayStore, smallViewport } from "../../stores";
|
||||
import { overlayStore } from "../../stores";
|
||||
|
||||
import EditChannel from "./EditChannel.svelte";
|
||||
import CreateChannel from "./CreateChannel.svelte";
|
||||
|
@ -9,6 +9,7 @@
|
|||
import EditMessage from "./EditMessage.svelte";
|
||||
import Settings from "./Settings.svelte";
|
||||
import Prompt from "./Prompt.svelte";
|
||||
import UserInfo from "./UserInfo.svelte";
|
||||
import AddCommunity from "./AddCommunity.svelte";
|
||||
import EditCommunity from "./EditCommunity.svelte";
|
||||
|
||||
|
@ -21,11 +22,12 @@
|
|||
5: EditMessage,
|
||||
6: Settings,
|
||||
7: Prompt,
|
||||
8: AddCommunity,
|
||||
9: EditCommunity,
|
||||
8: UserInfo,
|
||||
9: AddCommunity,
|
||||
10: EditCommunity,
|
||||
};
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
</label>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</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)),
|
||||
promoteUserSelf: method(103, true),
|
||||
putUserAvatar: method(104, true),
|
||||
getUser: withCacheable(method(105, true)),
|
||||
createChannel: method(200, true),
|
||||
updateChannelName: method(201, true),
|
||||
deleteChannel: method(202, true),
|
||||
|
@ -74,10 +73,6 @@ export function getErrorFromResponse(response) {
|
|||
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 };
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getItem } from "./storage";
|
||||
import { showSidebar, smallViewport, theme, usesKeyboardNavigation, overlayStore } from "./stores";
|
||||
import { showSidebar, smallViewport, theme, usesKeyboardNavigation } from "./stores";
|
||||
|
||||
function initViewportSizeHandler() {
|
||||
const root = document.querySelector(':root');
|
||||
|
@ -33,30 +33,22 @@ function updateTheme(themeName) {
|
|||
classes.add(`theme--${themeName}`);
|
||||
}
|
||||
|
||||
export function initResponsiveHandlers() {
|
||||
// Keyboard navigation detection
|
||||
function initKeyboardNavigationDetection() {
|
||||
document.addEventListener("keydown", ({ key }) => {
|
||||
if (key === "Tab") {
|
||||
usesKeyboardNavigation.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
const keyboardClickHandler = (e) => {
|
||||
document.addEventListener("click", e => {
|
||||
// screenX and screenY are 0 when a user presses enter for navigation
|
||||
usesKeyboardNavigation.set(!e.screenX && !e.screenY);
|
||||
};
|
||||
|
||||
const overlayClickHandler = () => {
|
||||
overlayStore.closeAllAbsolute();
|
||||
};
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
keyboardClickHandler(e);
|
||||
overlayClickHandler();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function initResponsiveHandlers() {
|
||||
initViewportSizeHandler();
|
||||
initKeyboardNavigationDetection();
|
||||
|
||||
const mediaQuery = window.matchMedia('(min-width: 768px)');
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import gateway, { GatewayEventType, GatewayPayloadType, GatewayPresenceStatus } from "./gateway";
|
||||
import logger from "./logging";
|
||||
import { CapabilityType, getGrantFor } from "./permissions";
|
||||
import { getMessageFromResponse, methods, remoteCall, remoteSignal, responseOk } from "./request";
|
||||
import { getItem, setItem } from "./storage";
|
||||
|
||||
|
@ -237,7 +236,7 @@ class MessageStore extends Store {
|
|||
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
|
||||
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;
|
||||
}
|
||||
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,
|
||||
Settings: 6,
|
||||
Prompt: 7,
|
||||
AddCommunity: 8,
|
||||
EditCommunity: 9
|
||||
UserInfo: 8,
|
||||
AddCommunity: 9,
|
||||
EditCommunity: 10
|
||||
};
|
||||
class OverlayStore extends Store {
|
||||
constructor() {
|
||||
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() {
|
||||
return !!this.value.length;
|
||||
}
|
||||
|
||||
pushAbsolute(type, x=0, y=0, props={}) {
|
||||
this.closeAllAbsolute();
|
||||
return this.push(type, {
|
||||
...props,
|
||||
place: {
|
||||
x, y
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
push(type, props={}) {
|
||||
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
|
||||
this.value.push({
|
||||
user: entry.user,
|
||||
bridgesTo: entry.bridgesTo,
|
||||
privacy: entry.privacy,
|
||||
terms: entry.terms
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -178,15 +178,6 @@ body {
|
|||
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 {
|
||||
background-color: var(--background-color-1);
|
||||
backdrop-filter: unset;
|
||||
|
@ -202,8 +193,6 @@ body {
|
|||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 650;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
@ -214,38 +203,25 @@ body {
|
|||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding: var(--space-norm);
|
||||
padding-top: var(--space-xs);
|
||||
background-color: transparent;
|
||||
background-color: var(--background-color-1);
|
||||
border-bottom-right-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 {
|
||||
margin-left: auto;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.modal-secondary-action {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.modal-backdrop-opaque .modal .modal-footer {
|
||||
background-color: var(--background-color-3);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.modal {
|
||||
width: 100%;
|
||||
|
@ -296,27 +272,6 @@ body {
|
|||
|
||||
/* 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 {
|
||||
color: var(--foreground-color-1);
|
||||
background: none;
|
||||
|
@ -328,7 +283,7 @@ body {
|
|||
border-radius: 9999px;
|
||||
font: inherit;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
|
@ -337,25 +292,34 @@ body {
|
|||
|
||||
.button-accent {
|
||||
color: var(--colored-element-text-color);
|
||||
background-color: var(--purple-1);
|
||||
}
|
||||
|
||||
.button-accent:hover {
|
||||
background-color: var(--purple-2);
|
||||
}
|
||||
|
||||
.button-accent:disabled {
|
||||
.button-accent:hover {
|
||||
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 {
|
||||
color: var(--red-2);
|
||||
}
|
||||
|
||||
.button-danger:hover {
|
||||
color: var(--red-1);
|
||||
}
|
||||
|
||||
/* icon buttons */
|
||||
|
||||
.icon-button {
|
||||
|
@ -567,11 +531,7 @@ body {
|
|||
border-radius: 9999px;
|
||||
font-size: x-small;
|
||||
margin-left: var(--space-sm);
|
||||
}
|
||||
|
||||
.user-badge.secondary {
|
||||
background-color: var(--background-color-1);
|
||||
border: 1px solid var(--background-color-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* util */
|
||||
|
|
|
@ -167,6 +167,9 @@ export class GatewayClient {
|
|||
lastAliveCheck: number;
|
||||
clientDispatchChannels: Set<string>;
|
||||
messagesSinceLastCheck: number;
|
||||
bridgesTo?: string;
|
||||
privacy?: string;
|
||||
terms?: string;
|
||||
|
||||
constructor(ws: WebSocket) {
|
||||
this.ws = ws;
|
||||
|
@ -176,6 +179,9 @@ export class GatewayClient {
|
|||
this.lastAliveCheck = performance.now();
|
||||
this.clientDispatchChannels = new Set();
|
||||
this.messagesSinceLastCheck = 0;
|
||||
this.bridgesTo = undefined;
|
||||
this.privacy = undefined;
|
||||
this.terms = undefined;
|
||||
|
||||
gatewayClients.add(this);
|
||||
this.ws.on("close", this.handleClose.bind(this));
|
||||
|
@ -237,6 +243,9 @@ export class GatewayClient {
|
|||
avatar: this.user.avatar
|
||||
},
|
||||
status,
|
||||
bridgesTo: this.bridgesTo,
|
||||
privacy: this.privacy,
|
||||
terms: this.terms,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { errors } from "../../errors";
|
|||
import { query } from "../../database";
|
||||
import { compare, hash } from "bcrypt";
|
||||
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 { randomBytes } from "crypto";
|
||||
import { unlink } from "fs/promises";
|
||||
|
@ -165,19 +165,3 @@ method(
|
|||
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,
|
||||
avatar: string | null
|
||||
},
|
||||
bridgesTo?: string,
|
||||
privacy?: string,
|
||||
terms?: string,
|
||||
status: GatewayPresenceStatus
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue