first implementation of "communities" + visual improvements
This commit is contained in:
parent
5a5588f1e3
commit
045e34cbd5
23 changed files with 576 additions and 144 deletions
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.main-container {
|
.main-container {
|
||||||
background-color: var(--background-color-1);
|
background-color: var(--background-color-2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -43,7 +43,7 @@ import { overlayStore, OverlayType, setMessageInputEvent } from "../stores";
|
||||||
}
|
}
|
||||||
|
|
||||||
.message:hover, .message.pinged {
|
.message:hover, .message.pinged {
|
||||||
background-color: var(--background-color-2);
|
background-color: var(--background-color-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
|
|
||||||
.message-input {
|
.message-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color : var(--background-color-2);
|
background-color : var(--background-color-3);
|
||||||
border: none;
|
border: none;
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|
|
@ -81,7 +81,6 @@
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: var(--background-color-1);
|
|
||||||
padding-top: var(--space-sm);
|
padding-top: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +105,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-left: var(--space-xs);
|
padding-left: var(--space-xs);
|
||||||
padding-right: var(--space-xs);
|
padding-right: var(--space-xs);
|
||||||
background-color: var(--background-color-1);
|
background-color: var(--background-color-2);
|
||||||
color: var(--foreground-color-2);
|
color: var(--foreground-color-2);
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,32 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: var(--h6);
|
font-size: var(--h6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.presence-sidebar {
|
||||||
|
min-width: 248px;
|
||||||
|
max-width: 248px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
<div class="sidebar-container presence-sidebar" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
||||||
<span class="online-label">ONLINE</span>
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
{#each $presenceStore as entry (entry.user.id)}
|
<span class="online-label">ONLINE</span>
|
||||||
<button class="sidebar-button">
|
|
||||||
<UserView size={28} user={entry.user}></UserView>
|
<div class="sidebar-buttons">
|
||||||
{#if entry.bridgesTo || entry.privacy || entry.terms}
|
{#each $presenceStore as entry (entry.user.id)}
|
||||||
<span class="user-badge" on:click={ () => overlayStore.push(OverlayType.UserInfo, { presenceEntry: entry }) }>SERVICE</span>
|
<button class="sidebar-button">
|
||||||
{/if}
|
<UserView size={28} user={entry.user}></UserView>
|
||||||
</button>
|
{#if entry.bridgesTo || entry.privacy || entry.terms}
|
||||||
{/each}
|
<span class="user-badge" on:click={ () => overlayStore.push(OverlayType.UserInfo, { presenceEntry: entry }) }>SERVICE</span>
|
||||||
{#if $smallViewport}
|
{/if}
|
||||||
<button on:click={ close } class="sidebar-button">
|
</button>
|
||||||
<div class="material-icons-outlined">arrow_back</div>
|
{/each}
|
||||||
<span class="sidebar-button-text">Back</span>
|
{#if $smallViewport}
|
||||||
</button>
|
<button on:click={ close } class="sidebar-button">
|
||||||
{/if}
|
<div class="material-icons-outlined">arrow_back</div>
|
||||||
|
<span class="sidebar-button-text">Back</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { quadInOut } from "svelte/easing";
|
import { quadInOut } from "svelte/easing";
|
||||||
import { maybeFly, maybeFlyIf } from "../animations";
|
import { maybeFly, maybeFlyIf } from "../animations";
|
||||||
import { channels, gatewayStatus, overlayStore, selectedChannel, showSidebar, smallViewport, userInfoStore, unreadStore, OverlayType } from "../stores";
|
import { avatarUrl } from "../storage";
|
||||||
|
import { channels, gatewayStatus, overlayStore, selectedChannel, showSidebar, smallViewport, userInfoStore, unreadStore, OverlayType, communities, selectedCommunity } from "../stores";
|
||||||
|
import AddCommunity from "./overlays/AddCommunity.svelte";
|
||||||
import UserView from "./UserView.svelte";
|
import UserView from "./UserView.svelte";
|
||||||
|
|
||||||
const selectChannel = (channel) => {
|
const selectChannel = (channel) => {
|
||||||
|
@ -13,46 +15,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: -10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
|
||||||
<div class="top-bar text-small text-bold">
|
|
||||||
<UserView size={28} user={$userInfoStore}></UserView>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar">
|
|
||||||
{#each $channels as channel (channel.id)}
|
|
||||||
<button on:click="{ selectChannel(channel) }" class="sidebar-button" class:selected={ channel.id === $selectedChannel.id }>
|
|
||||||
<span class="material-icons-outlined">tag</span>
|
|
||||||
<span class="sidebar-button-text">{ channel.name }</span>
|
|
||||||
<div class="sidebar-channel-buttons">
|
|
||||||
{#if $unreadStore.get(channel.id)}
|
|
||||||
<div class="unread-indicator">{ $unreadStore.get(channel.id) }</div>
|
|
||||||
{/if}
|
|
||||||
{#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}
|
|
||||||
{#if $userInfoStore && $userInfoStore.permissions.create_channel}
|
|
||||||
<button on:click="{ () => overlayStore.push(OverlayType.CreateChannel) }" class="sidebar-button">
|
|
||||||
<span class="material-icons-outlined">add</span>
|
|
||||||
<span class="sidebar-button-text">Create Channel</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button on:click="{ () => overlayStore.push(OverlayType.Settings) }" class="sidebar-button">
|
|
||||||
<span class="material-icons-outlined">settings</span>
|
|
||||||
<span class="sidebar-button-text">Settings</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if !$gatewayStatus.ready}
|
|
||||||
<div class="top-bar darker">
|
|
||||||
<span class="material-icons-outlined">cloud</span>
|
|
||||||
<span class="h5 top-bar-heading">connecting...</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.sidebar-channel-buttons {
|
.sidebar-channel-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -62,4 +24,108 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.communities {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--background-color-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities .button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--background-color-1);
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
border-radius: 50%;
|
||||||
|
contain: content;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: var(--space-sm);
|
||||||
|
margin-bottom: 0;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-property: border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities .button:hover, .communities .button.selected {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities .button span {
|
||||||
|
margin: 0.45em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities .button img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: -10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
||||||
|
<div class="communities">
|
||||||
|
<button class="button" on:click={() => selectedCommunity.clear()} class:selected={ $selectedCommunity.id === -1 }>
|
||||||
|
{#if $userInfoStore}
|
||||||
|
<img src={avatarUrl($userInfoStore.avatar, 64)} alt=" ">
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#each $communities as community (community.id)}
|
||||||
|
<button class="button" on:click={() => $selectedCommunity = community} class:selected={ $selectedCommunity.id === community.id }>
|
||||||
|
{#if community && community.avatar}
|
||||||
|
<img src={avatarUrl(community.avatar, 64)} alt=" ">
|
||||||
|
{:else}
|
||||||
|
<span>{ community.name[0] || "" }</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
<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)}>
|
||||||
|
<span class="material-icons-outlined">settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="top-bar text-small text-bold">
|
||||||
|
{#if $selectedCommunity.id !== -1}
|
||||||
|
{$selectedCommunity.name || ""}
|
||||||
|
{:else}
|
||||||
|
{$userInfoStore ? $userInfoStore.username || "" : ""}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-buttons">
|
||||||
|
{#each $channels as channel (channel.id)}
|
||||||
|
{#if ($selectedCommunity.id === -1 && channel.community_id === null) || ($selectedCommunity.id !== -1 && channel.community_id === $selectedCommunity.id)}
|
||||||
|
<button on:click="{ selectChannel(channel) }" class="sidebar-button" class:selected={ channel.id === $selectedChannel.id }>
|
||||||
|
<span class="material-icons-outlined">tag</span>
|
||||||
|
<span class="sidebar-button-text">{ channel.name }</span>
|
||||||
|
<div class="sidebar-channel-buttons">
|
||||||
|
{#if $unreadStore.get(channel.id)}
|
||||||
|
<div class="unread-indicator">{ $unreadStore.get(channel.id) }</div>
|
||||||
|
{/if}
|
||||||
|
{#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>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#if $userInfoStore && $userInfoStore.permissions.create_channel}
|
||||||
|
<button on:click="{ () => overlayStore.push(OverlayType.CreateChannel, { community: selectedCommunity.value }) }" class="sidebar-button">
|
||||||
|
<span class="material-icons-outlined">add</span>
|
||||||
|
<span class="sidebar-button-text">Create Channel</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if !$gatewayStatus.ready}
|
||||||
|
<div class="top-bar darker">
|
||||||
|
<span class="material-icons-outlined">cloud</span>
|
||||||
|
<span class="h5 top-bar-heading">connecting...</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
50
frontend/src/components/overlays/AddCommunity.svelte
Normal file
50
frontend/src/components/overlays/AddCommunity.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { overlayStore } from "../../stores";
|
||||||
|
import { methods, remoteSignal } from "../../request";
|
||||||
|
import { maybeModalFade, maybeModalScale } from "../../animations";
|
||||||
|
|
||||||
|
let communityName = "";
|
||||||
|
let createButtonEnabled = true;
|
||||||
|
export let close = () => {};
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
createButtonEnabled = false;
|
||||||
|
const { ok } = await remoteSignal(methods.createCommunity, communityName);
|
||||||
|
if (!ok) {
|
||||||
|
overlayStore.toast("Couldn't create community");
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
const onKeydown = async (e) => {
|
||||||
|
if (e.code !== "Enter")
|
||||||
|
return;
|
||||||
|
|
||||||
|
await create();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" transition:maybeModalFade on:click="{ close }" on:keydown="{ onKeydown }">
|
||||||
|
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="h4">Create Community</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<label class="input-label">
|
||||||
|
Community Name
|
||||||
|
<input class="input full-width" minlength="1" maxlength="32" bind:value={ communityName } />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,10 +6,11 @@
|
||||||
let channelName = "";
|
let channelName = "";
|
||||||
let createButtonEnabled = true;
|
let createButtonEnabled = true;
|
||||||
export let close = () => {};
|
export let close = () => {};
|
||||||
|
export let community = null;
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
createButtonEnabled = false;
|
createButtonEnabled = false;
|
||||||
const { ok } = await remoteSignal(methods.createChannel, channelName);
|
const { ok } = await remoteSignal(methods.createChannel, channelName, community.id !== -1 ? community.id : null);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
overlayStore.toast("Couldn't create channel");
|
overlayStore.toast("Couldn't create channel");
|
||||||
}
|
}
|
||||||
|
@ -33,6 +34,9 @@
|
||||||
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
|
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="h4">Create Channel</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
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 UserInfo from "./UserInfo.svelte";
|
||||||
|
import AddCommunity from "./AddCommunity.svelte";
|
||||||
|
|
||||||
const OverlayComponent = {
|
const OverlayComponent = {
|
||||||
0: CreateChannel,
|
0: CreateChannel,
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
6: Settings,
|
6: Settings,
|
||||||
7: Prompt,
|
7: Prompt,
|
||||||
8: UserInfo,
|
8: UserInfo,
|
||||||
|
9: AddCommunity,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,10 @@ export const GatewayPayloadType = {
|
||||||
PresenceUpdate: 140,
|
PresenceUpdate: 140,
|
||||||
|
|
||||||
UserUpdate: 150,
|
UserUpdate: 150,
|
||||||
|
|
||||||
|
CommunityCreate: 160,
|
||||||
|
CommunityUpdate: 161,
|
||||||
|
CommunityDelete: 162,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GatewayEventType = {
|
export const GatewayEventType = {
|
||||||
|
@ -59,6 +63,7 @@ export default {
|
||||||
heartbeatInterval: null,
|
heartbeatInterval: null,
|
||||||
user: null,
|
user: null,
|
||||||
channels: null,
|
channels: null,
|
||||||
|
communities: null,
|
||||||
reconnectDelay: 400,
|
reconnectDelay: 400,
|
||||||
reconnectTimeout: null,
|
reconnectTimeout: null,
|
||||||
handlers: new Map(),
|
handlers: new Map(),
|
||||||
|
@ -113,6 +118,7 @@ export default {
|
||||||
|
|
||||||
this.user = payload.d.user;
|
this.user = payload.d.user;
|
||||||
this.channels = payload.d.channels;
|
this.channels = payload.d.channels;
|
||||||
|
this.communities = payload.d.communities;
|
||||||
this.authenticated = true;
|
this.authenticated = true;
|
||||||
this.reconnectDelay = 400;
|
this.reconnectDelay = 400;
|
||||||
|
|
||||||
|
|
|
@ -5,23 +5,28 @@ const method = (methodId, requiresAuthentication) => ({methodId, requiresAuthent
|
||||||
const withCacheable = (method) => ({ ...method, cacheable: true })
|
const withCacheable = (method) => ({ ...method, cacheable: true })
|
||||||
|
|
||||||
export const methods = {
|
export const methods = {
|
||||||
// methodName: [ methodId, requiresAuthentication ]
|
createUser: method(100, false),
|
||||||
createUser: method(0, false),
|
loginUser: method(101, false),
|
||||||
loginUser: method(1, false),
|
getUserSelf: withCacheable(method(102, true)),
|
||||||
getUserSelf: withCacheable(method(2, true)),
|
promoteUserSelf: method(103, true),
|
||||||
promoteUserSelf: method(3, true),
|
putUserAvatar: method(104, true),
|
||||||
putUserAvatar: method(4, true),
|
createChannel: method(200, true),
|
||||||
createChannel: method(5, true),
|
updateChannelName: method(201, true),
|
||||||
updateChannelName: method(6, true),
|
deleteChannel: method(202, true),
|
||||||
deleteChannel: method(7, true),
|
getChannel: withCacheable(method(203, true)),
|
||||||
getChannel: withCacheable(method(8, true)),
|
getChannels: withCacheable(method(204, true)),
|
||||||
getChannels: withCacheable(method(9, true)),
|
createChannelMessage: method(205, true),
|
||||||
createChannelMessage: method(10, true),
|
getChannelMessages: withCacheable(method(206, true)),
|
||||||
getChannelMessages: withCacheable(method(11, true)),
|
putChannelTyping: method(207, true),
|
||||||
putChannelTyping: method(12, true),
|
deleteMessage: method(300, true),
|
||||||
deleteMessage: method(13, true),
|
updateMessageContent: method(301, true),
|
||||||
updateMessageContent: method(14, true),
|
getMessage: withCacheable(method(302, true)),
|
||||||
getMessage: withCacheable(method(15, true))
|
createCommunity: method(400, true),
|
||||||
|
updateCommunityName: method(401, true),
|
||||||
|
deleteCommunity: method(402, true),
|
||||||
|
getCommunity: withCacheable(method(403, true)),
|
||||||
|
getCommunities: withCacheable(method(404, true)),
|
||||||
|
getCommunityChannels: withCacheable(method(405, true)),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function compatibleFetch(endpoint, options) {
|
export function compatibleFetch(endpoint, options) {
|
||||||
|
|
|
@ -5,12 +5,13 @@ const defaults = {
|
||||||
"auth:token": "",
|
"auth:token": "",
|
||||||
"ui:doAnimations": true,
|
"ui:doAnimations": true,
|
||||||
"ui:theme": "dark",
|
"ui:theme": "dark",
|
||||||
"state:openChannelId": -1,
|
"state:openCommunityId": -1,
|
||||||
|
"state:selectedChannels": {},
|
||||||
"log:Gateway": false,
|
"log:Gateway": false,
|
||||||
"log:Store": false,
|
"log:Store": false,
|
||||||
"log:Messages": false,
|
"log:Messages": false,
|
||||||
"log:Timeline": false,
|
"log:Timeline": false,
|
||||||
"ui:stateful:presistSelectedChannel": true,
|
"ui:stateful:presistSelection": true,
|
||||||
"ui:showSidebarToggle": false,
|
"ui:showSidebarToggle": false,
|
||||||
"ui:alwaysUseMobileChatBar": false,
|
"ui:alwaysUseMobileChatBar": false,
|
||||||
"ui:online:processRemoteTypingEvents": true,
|
"ui:online:processRemoteTypingEvents": true,
|
||||||
|
|
|
@ -139,21 +139,6 @@ class ChannelsStore extends Store {
|
||||||
|
|
||||||
gateway.subscribe(GatewayEventType.Ready, ({ channels }) => {
|
gateway.subscribe(GatewayEventType.Ready, ({ channels }) => {
|
||||||
this.value = channels;
|
this.value = channels;
|
||||||
if (getItem("ui:stateful:presistSelectedChannel")) {
|
|
||||||
selectedChannel.value.id = getItem("state:openChannelId");
|
|
||||||
}
|
|
||||||
if (channels.length >= 1) {
|
|
||||||
if (!selectedChannel.value || selectedChannel.value.id === -1) {
|
|
||||||
selectedChannel.set(channels[0]);
|
|
||||||
} else {
|
|
||||||
// if a channel id is already selected, we'll populate it with the data we just got from the gateway
|
|
||||||
const index = this.value.findIndex(e => e.id === selectedChannel.value.id);
|
|
||||||
if (index !== -1)
|
|
||||||
selectedChannel.set(this.value[index]);
|
|
||||||
else // if the channel doesn't exist, just select the first one
|
|
||||||
selectedChannel.set(channels[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.updated();
|
this.updated();
|
||||||
});
|
});
|
||||||
gateway.subscribe(GatewayEventType.ChannelCreate, (channel) => {
|
gateway.subscribe(GatewayEventType.ChannelCreate, (channel) => {
|
||||||
|
@ -414,7 +399,8 @@ export const OverlayType = {
|
||||||
EditMessage: 5,
|
EditMessage: 5,
|
||||||
Settings: 6,
|
Settings: 6,
|
||||||
Prompt: 7,
|
Prompt: 7,
|
||||||
UserInfo: 8
|
UserInfo: 8,
|
||||||
|
AddCommunity: 9
|
||||||
};
|
};
|
||||||
class OverlayStore extends Store {
|
class OverlayStore extends Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -672,7 +658,161 @@ class PluginStore extends Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectedChannel = new Store({ id: -1, name: "none", creator_id: -1 }, "selectedChannel");
|
class CommunitiesStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(gateway.communities || [], "CommunitiesStore");
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.Ready, ({ communities }) => {
|
||||||
|
this.value = communities;
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityCreate, (community) => {
|
||||||
|
this.value.push(community);
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityDelete, ({ id }) => {
|
||||||
|
const index = this.value.findIndex(e => e.id === id);
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.value.splice(index, 1);
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityUpdate, (data) => {
|
||||||
|
const index = this.value.findIndex(e => e.id === data.id);
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
|
if (!this.value[index])
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.value[index] = data;
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noneChannel = {id: -1, name: "none", creator_id: -1}
|
||||||
|
class SelectedChannelStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(noneChannel, "SelectedChannelStore");
|
||||||
|
|
||||||
|
this.communityIdToSelectedChannel = new Map();
|
||||||
|
|
||||||
|
selectedCommunity.subscribe((community) => {
|
||||||
|
this.value = this.communityIdToSelectedChannel.get(community.id) || noneChannel;
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.Ready, ({ channels }) => {
|
||||||
|
this.communityIdToSelectedChannel.clear();
|
||||||
|
const savedMap = getItem("state:selectedChannels");
|
||||||
|
for (const [communityId, channelId] of Object.entries(savedMap)) {
|
||||||
|
const channel = channels.find(c => c.id === channelId);
|
||||||
|
if (channel) {
|
||||||
|
this.communityIdToSelectedChannel.set(parseInt(communityId), channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(this.communityIdToSelectedChannel);
|
||||||
|
this.value = this.communityIdToSelectedChannel.get(selectedCommunity.value.id) || noneChannel;
|
||||||
|
this.updateSavedMap();
|
||||||
|
this.updated();
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityDelete, ({ id }) => {
|
||||||
|
if (this.communityIdToSelectedChannel.delete(id) && this.value && this.value.id === id) {
|
||||||
|
this.value = noneChannel;
|
||||||
|
this.updateSavedMap();
|
||||||
|
this.updated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.ChannelDelete, ({ id }) => {
|
||||||
|
this.communityIdToSelectedChannel.forEach((channel, communityId) => {
|
||||||
|
if (channel.id === id) {
|
||||||
|
if (this.communityIdToSelectedChannel.delete(communityId) && this.value && this.value.id === id) {
|
||||||
|
this.value = noneChannel;
|
||||||
|
this.updateSavedMap();
|
||||||
|
this.updated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
gateway.subscribe(GatewayEventType.ChannelUpdate, (data) => {
|
||||||
|
if (this.value && this.value.id === data.id) {
|
||||||
|
this.value = data;
|
||||||
|
this.communityIdToSelectedChannel.set(selectedCommunity.value.id, data);
|
||||||
|
this.updateSavedMap();
|
||||||
|
this.updated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSavedMap() {
|
||||||
|
if (getItem("ui:stateful:presistSelection")) {
|
||||||
|
const value = {};
|
||||||
|
this.communityIdToSelectedChannel.forEach((channel, communityId) => {
|
||||||
|
if (!channel || channel.id === -1) return;
|
||||||
|
value[communityId] = channel.id;
|
||||||
|
});
|
||||||
|
setItem("state:selectedChannels", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
if (value === this.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
|
this.communityIdToSelectedChannel.set(selectedCommunity.value.id, value);
|
||||||
|
this.updateSavedMap();
|
||||||
|
this.updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectedCommunityStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super({ id: -1, name: "none", creator_id: -1, created_at: 0, avatar: null }, "SelectedCommunityStore");
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.Ready, ({ communities }) => {
|
||||||
|
if (getItem("ui:stateful:presistSelection")) {
|
||||||
|
this.value.id = getItem("state:openCommunityId");
|
||||||
|
}
|
||||||
|
if (communities.length > 0 && this.value.id !== -1) {
|
||||||
|
const index = communities.findIndex(e => e.id === this.value.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.set(communities[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityDelete, ({ id }) => {
|
||||||
|
if (this.value.id === id) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.CommunityUpdate, (data) => {
|
||||||
|
if (data.id === this.value.id) {
|
||||||
|
this.value = data;
|
||||||
|
this.updated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.on(value => {
|
||||||
|
if (getItem("ui:stateful:presistSelection")) {
|
||||||
|
setItem("state:openCommunityId", value.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.value = { id: -1, name: "none", creator_id: -1, created_at: 0, avatar: null };
|
||||||
|
this.updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const selectedCommunity = new SelectedCommunityStore();
|
||||||
|
export const selectedChannel = new SelectedChannelStore();
|
||||||
export const showSidebar = new Store(true, "showSidebar");
|
export const showSidebar = new Store(true, "showSidebar");
|
||||||
export const showPresenceSidebar = new Store(false, "showPresenceSidebar");
|
export const showPresenceSidebar = new Store(false, "showPresenceSidebar");
|
||||||
export const smallViewport = new Store(false, "smallViewport");
|
export const smallViewport = new Store(false, "smallViewport");
|
||||||
|
@ -680,6 +820,7 @@ export const showChannelView = new Store(true, "showChannelView");
|
||||||
export const theme = new StorageItemStore("ui:theme");
|
export const theme = new StorageItemStore("ui:theme");
|
||||||
export const doAnimations = new StorageItemStore("ui:doAnimations");
|
export const doAnimations = new StorageItemStore("ui:doAnimations");
|
||||||
export const channels = new ChannelsStore();
|
export const channels = new ChannelsStore();
|
||||||
|
export const communities = new CommunitiesStore();
|
||||||
export const gatewayStatus = new GatewayStatusStore();
|
export const gatewayStatus = new GatewayStatusStore();
|
||||||
export const messagesStoreProvider = new MessagesStoreProvider();
|
export const messagesStoreProvider = new MessagesStoreProvider();
|
||||||
export const userInfoStore = new UserInfoStore();
|
export const userInfoStore = new UserInfoStore();
|
||||||
|
@ -741,12 +882,6 @@ export const allStores = {
|
||||||
sendMessageAction,
|
sendMessageAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
selectedChannel.on((newSelectedChannel) => {
|
|
||||||
if (getItem("ui:stateful:presistSelectedChannel")) {
|
|
||||||
setItem("state:openChannelId", newSelectedChannel.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
unreadStore.subscribe(() => {
|
unreadStore.subscribe(() => {
|
||||||
let totalUnreads = 0;
|
let totalUnreads = 0;
|
||||||
unreadStore.value.forEach(count => totalUnreads += count);
|
unreadStore.value.forEach(count => totalUnreads += count);
|
||||||
|
|
|
@ -34,11 +34,10 @@
|
||||||
/* top-level */
|
/* top-level */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background-color-0: hsl(180, 11%, 4%);
|
--background-color-0: #151515;
|
||||||
--background-color-1: hsl(180, 11%, 5%);
|
--background-color-1: #1a1a1a;
|
||||||
--background-color-2: hsl(180, 11%, 8%);
|
--background-color-2: #202020;
|
||||||
--background-color-3: hsl(180, 11%, 11%);
|
--background-color-3: #262626;
|
||||||
--background-color-4: hsl(180, 11%, 15%);
|
|
||||||
--foreground-color-1: hsl(210, 100%, 100%);
|
--foreground-color-1: hsl(210, 100%, 100%);
|
||||||
--foreground-color-2: hsl(63, 10%, 82%);
|
--foreground-color-2: hsl(63, 10%, 82%);
|
||||||
--foreground-color-3: hsl(63, 2%, 60%);
|
--foreground-color-3: hsl(63, 2%, 60%);
|
||||||
|
@ -144,6 +143,7 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--space-sm);
|
padding: var(--space-sm);
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-bar-heading {
|
.top-bar-heading {
|
||||||
|
@ -413,16 +413,22 @@ body {
|
||||||
.h5 {font-size: var(--h5)}
|
.h5 {font-size: var(--h5)}
|
||||||
.text-small {font-size: var(--h6)}
|
.text-small {font-size: var(--h6)}
|
||||||
.text-bold {font-weight: 700}
|
.text-bold {font-weight: 700}
|
||||||
|
.text-fg-2 {
|
||||||
|
color: var(--foreground-color-2);
|
||||||
|
}
|
||||||
|
.text-fg-3 {
|
||||||
|
color: var(--foreground-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
/* sidebar */
|
/* sidebar */
|
||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
background-color: var(--background-color-0);
|
background-color: var(--background-color-0);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 255px;
|
min-width: 320px;
|
||||||
max-width: 255px;
|
max-width: 320px;
|
||||||
contain: content;
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,6 +441,13 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: var(--background-color-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: var(--space-xs);
|
padding: var(--space-xs);
|
||||||
|
@ -450,7 +463,7 @@ body {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: var(--background-color-0);
|
background-color: var(--background-color-1);
|
||||||
padding: var(--space-xs);
|
padding: var(--space-xs);
|
||||||
margin-bottom: var(--space-xxs);
|
margin-bottom: var(--space-xxs);
|
||||||
color: var(--foreground-special-color-1);
|
color: var(--foreground-special-color-1);
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const query = function(text: string, params: any[] = [], rejectOnError =
|
||||||
if (rejectOnError) {
|
if (rejectOnError) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
|
console.error(error);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,10 +27,22 @@ export default async function databaseInit() {
|
||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
`
|
`
|
||||||
ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT NULL;
|
ALTER TABLE messages ADD COLUMN IF NOT EXISTS nick_username VARCHAR(64) DEFAULT NULL;
|
||||||
`,
|
`,
|
||||||
`
|
`
|
||||||
ALTER TABLE users ADD COLUMN avatar VARCHAR(48) DEFAULT NULL;
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar VARCHAR(48) DEFAULT NULL;
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS communities(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(64) NOT NULL,
|
||||||
|
owner_id SERIAL REFERENCES users ON DELETE CASCADE,
|
||||||
|
avatar VARCHAR(48) DEFAULT NULL,
|
||||||
|
created_at BIGINT
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
ALTER TABLE channels ADD COLUMN IF NOT EXISTS community_id INT NULL REFERENCES communities(id) ON DELETE CASCADE;
|
||||||
`
|
`
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,10 @@ export enum GatewayPayloadType {
|
||||||
PresenceUpdate = 140,
|
PresenceUpdate = 140,
|
||||||
|
|
||||||
UserUpdate = 150,
|
UserUpdate = 150,
|
||||||
|
|
||||||
|
CommunityCreate = 160,
|
||||||
|
CommunityUpdate,
|
||||||
|
CommunityDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GatewayPresenceStatus {
|
export enum GatewayPresenceStatus {
|
||||||
|
|
|
@ -381,23 +381,30 @@ export default function(server: Server) {
|
||||||
if ((sessions.length + 1) > MAX_GATEWAY_SESSIONS_PER_USER) {
|
if ((sessions.length + 1) > MAX_GATEWAY_SESSIONS_PER_USER) {
|
||||||
return closeWithError(ws, gatewayErrors.TOO_MANY_SESSIONS);
|
return closeWithError(ws, gatewayErrors.TOO_MANY_SESSIONS);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// TODO: each user should have their own list of channels that they join
|
||||||
|
const [channels, communities] = await Promise.all([
|
||||||
|
query("SELECT id, name, owner_id, community_id FROM channels ORDER BY id ASC"),
|
||||||
|
query("SELECT id, name, owner_id, avatar, created_at FROM communities ORDER BY id ASC"),
|
||||||
|
]);
|
||||||
|
if (!channels || !communities) {
|
||||||
|
return closeWithError(ws, gatewayErrors.GOT_NO_DATABASE_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessions) {
|
||||||
sessions = [];
|
sessions = [];
|
||||||
sessionsByUserId.set(user.id, sessions);
|
sessionsByUserId.set(user.id, sessions);
|
||||||
}
|
}
|
||||||
sessions.push(ws);
|
sessions.push(ws);
|
||||||
|
|
||||||
// TODO: each user should have their own list of channels that they join
|
|
||||||
const channels = await query("SELECT id, name, owner_id FROM channels ORDER BY id ASC");
|
|
||||||
|
|
||||||
if (!channels) {
|
|
||||||
return closeWithError(ws, gatewayErrors.GOT_NO_DATABASE_DATA);
|
|
||||||
}
|
|
||||||
|
|
||||||
clientSubscribe(ws, "*");
|
clientSubscribe(ws, "*");
|
||||||
channels.rows.forEach(c => {
|
for (let i = 0; i < channels.rows.length; i++) {
|
||||||
clientSubscribe(ws, `channel:${c.id}`);
|
clientSubscribe(ws, `channel:${channels.rows[i].id}`);
|
||||||
});
|
}
|
||||||
|
for (let i = 0; i < communities.rows.length; i++) {
|
||||||
|
clientSubscribe(ws, `community:${communities.rows[i].id}`);
|
||||||
|
}
|
||||||
|
|
||||||
ws.state.user = user;
|
ws.state.user = user;
|
||||||
ws.state.bridgesTo = authData.bridgesTo;
|
ws.state.bridgesTo = authData.bridgesTo;
|
||||||
|
@ -419,6 +426,7 @@ export default function(server: Server) {
|
||||||
d: {
|
d: {
|
||||||
user: getPublicUserObject(ws.state.user),
|
user: getPublicUserObject(ws.state.user),
|
||||||
channels: channels.rows,
|
channels: channels.rows,
|
||||||
|
communities: communities.rows,
|
||||||
presence: getInitialPresenceEntries()
|
presence: getInitialPresenceEntries()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,13 +10,13 @@ import serverConfig from "../../serverconfig";
|
||||||
|
|
||||||
method(
|
method(
|
||||||
"createChannel",
|
"createChannel",
|
||||||
[withRegexp(channelNameRegex, string(1, 32))],
|
[withRegexp(channelNameRegex, string(1, 32)), withOptional(uint())],
|
||||||
async (user: User, name: string) => {
|
async (user: User, name: string, communityId: number | null) => {
|
||||||
if (serverConfig.superuserRequirement.createChannel && !user.is_superuser) {
|
if (serverConfig.superuserRequirement.createChannel && !user.is_superuser) {
|
||||||
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, user.id]);
|
const result = await query("INSERT INTO channels(name, owner_id, community_id) VALUES ($1, $2, $3) RETURNING id, name, owner_id, community_id", [name, user.id, communityId]);
|
||||||
if (!result || result.rowCount < 1) {
|
if (!result || result.rowCount < 1) {
|
||||||
return errors.GOT_NO_DATABASE_DATA;
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
}
|
}
|
||||||
|
@ -46,23 +46,17 @@ method(
|
||||||
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await query("UPDATE channels SET name = $1 WHERE id = $2", [name, id]);
|
const result = await query("UPDATE channels SET name = $1 WHERE id = $2 RETURNING id, name, owner_id, community_id", [name, id]);
|
||||||
if (!result || result.rowCount < 1) {
|
if (!result || result.rowCount < 1) {
|
||||||
return errors.GOT_NO_DATABASE_DATA;
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePayload = {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
owner_id: permissionCheckResult.rows[0].owner_id
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(`channel:${id}`, {
|
dispatch(`channel:${id}`, {
|
||||||
t: GatewayPayloadType.ChannelUpdate,
|
t: GatewayPayloadType.ChannelUpdate,
|
||||||
d: updatePayload
|
d: result.rows[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatePayload;
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -96,7 +90,7 @@ method(
|
||||||
"getChannel",
|
"getChannel",
|
||||||
[uint()],
|
[uint()],
|
||||||
async (_user: User, id: number) => {
|
async (_user: User, id: number) => {
|
||||||
const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]);
|
const result = await query("SELECT id, name, owner_id, community_id FROM channels WHERE id = $1", [id]);
|
||||||
if (!result || result.rowCount < 1) {
|
if (!result || result.rowCount < 1) {
|
||||||
return errors.NOT_FOUND;
|
return errors.NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +103,7 @@ method(
|
||||||
"getChannels",
|
"getChannels",
|
||||||
[],
|
[],
|
||||||
async (_user: User) => {
|
async (_user: User) => {
|
||||||
const result = await query("SELECT id, name, owner_id FROM channels");
|
const result = await query("SELECT id, name, owner_id, community_id FROM channels");
|
||||||
|
|
||||||
return (result && result.rows) ? result.rows : [];
|
return (result && result.rows) ? result.rows : [];
|
||||||
}
|
}
|
||||||
|
|
114
src/rpc/apis/communities.ts
Normal file
114
src/rpc/apis/communities.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import { channelNameRegex, method, int, string, uint, withRegexp } from "../rpc";
|
||||||
|
import { query } from "../../database";
|
||||||
|
import { errors } from "../../errors";
|
||||||
|
import { dispatch, dispatchChannelSubscribe } from "../../gateway";
|
||||||
|
import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype";
|
||||||
|
import serverConfig from "../../serverconfig";
|
||||||
|
|
||||||
|
method(
|
||||||
|
"createCommunity",
|
||||||
|
[withRegexp(channelNameRegex, string(1, 64))],
|
||||||
|
async (user: User, name: string) => {
|
||||||
|
if (serverConfig.superuserRequirement.createChannel && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
const result = await query("INSERT INTO communities(name, owner_id, created_at) VALUES ($1, $2, $3) RETURNING id, name, owner_id, avatar, created_at", [name, user.id, Date.now().toString()]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch("*", {
|
||||||
|
t: GatewayPayloadType.CommunityCreate,
|
||||||
|
d: result.rows[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchChannelSubscribe("*", `community:${result.rows[0].id}`);
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"updateCommunityName",
|
||||||
|
[uint(), withRegexp(channelNameRegex, string(1, 32))],
|
||||||
|
async (user: User, id: number, name: string) => {
|
||||||
|
const permissionCheckResult = await query("SELECT owner_id FROM communities WHERE id = $1", [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].owner_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("UPDATE communities SET name = $1 WHERE id = $2 RETURNING id, name, owner_id, avatar, created_at", [name, id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(`community:${id}`, {
|
||||||
|
t: GatewayPayloadType.CommunityUpdate,
|
||||||
|
d: result.rows[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"deleteCommunity",
|
||||||
|
[uint()],
|
||||||
|
async (user: User, id: number) => {
|
||||||
|
const permissionCheckResult = await query("SELECT owner_id FROM communities WHERE id = $1", [id]);
|
||||||
|
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (permissionCheckResult.rows[0].owner_id !== user.id && !user.is_superuser) {
|
||||||
|
return errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query("DELETE FROM communities WHERE id = $1", [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.GOT_NO_DATABASE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(`community:${id}`, {
|
||||||
|
t: GatewayPayloadType.CommunityDelete,
|
||||||
|
d: {id}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {id};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getCommunity",
|
||||||
|
[uint()],
|
||||||
|
async (_user: User, id: number) => {
|
||||||
|
const result = await query("SELECT id, name, owner_id, avatar, created_at FROM communities WHERE id = $1", [id]);
|
||||||
|
if (!result || result.rowCount < 1) {
|
||||||
|
return errors.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getCommunities",
|
||||||
|
[],
|
||||||
|
async (_user: User) => {
|
||||||
|
const result = await query("SELECT id, name, owner_id, avatar, created_at FROM communities");
|
||||||
|
|
||||||
|
return (result && result.rows) ? result.rows : [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
method(
|
||||||
|
"getCommunityChannels",
|
||||||
|
[uint()],
|
||||||
|
async (_user: User, id: number) => {
|
||||||
|
const result = await query("SELECT id, name, owner_id, community_id FROM channels WHERE community_id = $1", [id]);
|
||||||
|
|
||||||
|
return (result && result.rows) ? result.rows : [];
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,8 +1,14 @@
|
||||||
import "./rpc";
|
import { methodGroup, methodNameToId, methods } from "./rpc";
|
||||||
|
|
||||||
|
methodGroup(100);
|
||||||
import "./apis/users";
|
import "./apis/users";
|
||||||
|
methodGroup(200);
|
||||||
import "./apis/channels";
|
import "./apis/channels";
|
||||||
|
methodGroup(300);
|
||||||
import "./apis/messages";
|
import "./apis/messages";
|
||||||
import { methodNameToId, methods } from "./rpc";
|
methodGroup(400);
|
||||||
|
import "./apis/communities";
|
||||||
|
|
||||||
|
|
||||||
console.log("--- begin rpc method map ---")
|
console.log("--- begin rpc method map ---")
|
||||||
const methodMap: any = Object.fromEntries(methodNameToId);
|
const methodMap: any = Object.fromEntries(methodNameToId);
|
||||||
|
|
|
@ -49,6 +49,10 @@ export const methods: Map<number, RPCMethod> = new Map();
|
||||||
export const methodNameToId: Map<string, number> = new Map();
|
export const methodNameToId: Map<string, number> = new Map();
|
||||||
let lastMethodId = 0;
|
let lastMethodId = 0;
|
||||||
|
|
||||||
|
export const methodGroup = (origin: number) => {
|
||||||
|
lastMethodId = origin;
|
||||||
|
}
|
||||||
|
|
||||||
export const method = (name: string, args: RPCArgument[], func: ((...args: any[]) => any), requiresAuthentication: boolean = true) => {
|
export const method = (name: string, args: RPCArgument[], func: ((...args: any[]) => any), requiresAuthentication: boolean = true) => {
|
||||||
let id = lastMethodId++;
|
let id = lastMethodId++;
|
||||||
methodNameToId.set(name, id);
|
methodNameToId.set(name, id);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import rpcRouter from "./routes/api/v1/rpc";
|
||||||
import matrixRouter from "./routes/matrix";
|
import matrixRouter from "./routes/matrix";
|
||||||
import { errors } from "./errors";
|
import { errors } from "./errors";
|
||||||
|
|
||||||
const ENABLE_MATRIX_LAYER = false;
|
const ENABLE_MATRIX_LAYER = true;
|
||||||
|
|
||||||
export default function(app: Application) {
|
export default function(app: Application) {
|
||||||
app.use(json());
|
app.use(json());
|
||||||
|
|
Loading…
Reference in a new issue