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>
|
||||
.main-container {
|
||||
background-color: var(--background-color-1);
|
||||
background-color: var(--background-color-2);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -43,7 +43,7 @@ import { overlayStore, OverlayType, setMessageInputEvent } from "../stores";
|
|||
}
|
||||
|
||||
.message:hover, .message.pinged {
|
||||
background-color: var(--background-color-2);
|
||||
background-color: var(--background-color-3);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
background-color : var(--background-color-2);
|
||||
background-color : var(--background-color-3);
|
||||
border: none;
|
||||
color: currentColor;
|
||||
border-radius: var(--radius-md);
|
||||
|
|
|
@ -81,7 +81,6 @@
|
|||
flex-grow: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--background-color-1);
|
||||
padding-top: var(--space-sm);
|
||||
}
|
||||
|
||||
|
@ -106,7 +105,7 @@
|
|||
padding: 0;
|
||||
padding-left: 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);
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
|
|
@ -18,11 +18,18 @@
|
|||
font-weight: 700;
|
||||
font-size: var(--h6);
|
||||
}
|
||||
|
||||
.presence-sidebar {
|
||||
min-width: 248px;
|
||||
max-width: 248px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="sidebar-container" 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-container presence-sidebar" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
|
||||
<div class="sidebar">
|
||||
<span class="online-label">ONLINE</span>
|
||||
|
||||
<div class="sidebar-buttons">
|
||||
{#each $presenceStore as entry (entry.user.id)}
|
||||
<button class="sidebar-button">
|
||||
<UserView size={28} user={entry.user}></UserView>
|
||||
|
@ -38,4 +45,5 @@
|
|||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { quadInOut } from "svelte/easing";
|
||||
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";
|
||||
|
||||
const selectChannel = (channel) => {
|
||||
|
@ -13,12 +15,89 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sidebar-channel-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
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>
|
||||
|
||||
<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 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>
|
||||
|
@ -33,17 +112,14 @@
|
|||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if $userInfoStore && $userInfoStore.permissions.create_channel}
|
||||
<button on:click="{ () => overlayStore.push(OverlayType.CreateChannel) }" class="sidebar-button">
|
||||
<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}
|
||||
<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">
|
||||
|
@ -51,15 +127,5 @@
|
|||
<span class="h5 top-bar-heading">connecting...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar-channel-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
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 createButtonEnabled = true;
|
||||
export let close = () => {};
|
||||
export let community = null;
|
||||
|
||||
const create = async () => {
|
||||
createButtonEnabled = false;
|
||||
const { ok } = await remoteSignal(methods.createChannel, channelName);
|
||||
const { ok } = await remoteSignal(methods.createChannel, channelName, community.id !== -1 ? community.id : null);
|
||||
if (!ok) {
|
||||
overlayStore.toast("Couldn't create channel");
|
||||
}
|
||||
|
@ -33,6 +34,9 @@
|
|||
<div class="modal" transition:maybeModalScale on:click|stopPropagation>
|
||||
<div class="modal-header">
|
||||
<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 class="modal-content">
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import Settings from "./Settings.svelte";
|
||||
import Prompt from "./Prompt.svelte";
|
||||
import UserInfo from "./UserInfo.svelte";
|
||||
import AddCommunity from "./AddCommunity.svelte";
|
||||
|
||||
const OverlayComponent = {
|
||||
0: CreateChannel,
|
||||
|
@ -21,6 +22,7 @@
|
|||
6: Settings,
|
||||
7: Prompt,
|
||||
8: UserInfo,
|
||||
9: AddCommunity,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -35,6 +35,10 @@ export const GatewayPayloadType = {
|
|||
PresenceUpdate: 140,
|
||||
|
||||
UserUpdate: 150,
|
||||
|
||||
CommunityCreate: 160,
|
||||
CommunityUpdate: 161,
|
||||
CommunityDelete: 162,
|
||||
}
|
||||
|
||||
export const GatewayEventType = {
|
||||
|
@ -59,6 +63,7 @@ export default {
|
|||
heartbeatInterval: null,
|
||||
user: null,
|
||||
channels: null,
|
||||
communities: null,
|
||||
reconnectDelay: 400,
|
||||
reconnectTimeout: null,
|
||||
handlers: new Map(),
|
||||
|
@ -113,6 +118,7 @@ export default {
|
|||
|
||||
this.user = payload.d.user;
|
||||
this.channels = payload.d.channels;
|
||||
this.communities = payload.d.communities;
|
||||
this.authenticated = true;
|
||||
this.reconnectDelay = 400;
|
||||
|
||||
|
|
|
@ -5,23 +5,28 @@ const method = (methodId, requiresAuthentication) => ({methodId, requiresAuthent
|
|||
const withCacheable = (method) => ({ ...method, cacheable: true })
|
||||
|
||||
export const methods = {
|
||||
// methodName: [ methodId, requiresAuthentication ]
|
||||
createUser: method(0, false),
|
||||
loginUser: method(1, false),
|
||||
getUserSelf: withCacheable(method(2, true)),
|
||||
promoteUserSelf: method(3, true),
|
||||
putUserAvatar: method(4, true),
|
||||
createChannel: method(5, true),
|
||||
updateChannelName: method(6, true),
|
||||
deleteChannel: method(7, true),
|
||||
getChannel: withCacheable(method(8, true)),
|
||||
getChannels: withCacheable(method(9, true)),
|
||||
createChannelMessage: method(10, true),
|
||||
getChannelMessages: withCacheable(method(11, true)),
|
||||
putChannelTyping: method(12, true),
|
||||
deleteMessage: method(13, true),
|
||||
updateMessageContent: method(14, true),
|
||||
getMessage: withCacheable(method(15, true))
|
||||
createUser: method(100, false),
|
||||
loginUser: method(101, false),
|
||||
getUserSelf: withCacheable(method(102, true)),
|
||||
promoteUserSelf: method(103, true),
|
||||
putUserAvatar: method(104, true),
|
||||
createChannel: method(200, true),
|
||||
updateChannelName: method(201, true),
|
||||
deleteChannel: method(202, true),
|
||||
getChannel: withCacheable(method(203, true)),
|
||||
getChannels: withCacheable(method(204, true)),
|
||||
createChannelMessage: method(205, true),
|
||||
getChannelMessages: withCacheable(method(206, true)),
|
||||
putChannelTyping: method(207, true),
|
||||
deleteMessage: method(300, true),
|
||||
updateMessageContent: method(301, true),
|
||||
getMessage: withCacheable(method(302, 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) {
|
||||
|
|
|
@ -5,12 +5,13 @@ const defaults = {
|
|||
"auth:token": "",
|
||||
"ui:doAnimations": true,
|
||||
"ui:theme": "dark",
|
||||
"state:openChannelId": -1,
|
||||
"state:openCommunityId": -1,
|
||||
"state:selectedChannels": {},
|
||||
"log:Gateway": false,
|
||||
"log:Store": false,
|
||||
"log:Messages": false,
|
||||
"log:Timeline": false,
|
||||
"ui:stateful:presistSelectedChannel": true,
|
||||
"ui:stateful:presistSelection": true,
|
||||
"ui:showSidebarToggle": false,
|
||||
"ui:alwaysUseMobileChatBar": false,
|
||||
"ui:online:processRemoteTypingEvents": true,
|
||||
|
|
|
@ -139,21 +139,6 @@ class ChannelsStore extends Store {
|
|||
|
||||
gateway.subscribe(GatewayEventType.Ready, ({ 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();
|
||||
});
|
||||
gateway.subscribe(GatewayEventType.ChannelCreate, (channel) => {
|
||||
|
@ -414,7 +399,8 @@ export const OverlayType = {
|
|||
EditMessage: 5,
|
||||
Settings: 6,
|
||||
Prompt: 7,
|
||||
UserInfo: 8
|
||||
UserInfo: 8,
|
||||
AddCommunity: 9
|
||||
};
|
||||
class OverlayStore extends Store {
|
||||
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 showPresenceSidebar = new Store(false, "showPresenceSidebar");
|
||||
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 doAnimations = new StorageItemStore("ui:doAnimations");
|
||||
export const channels = new ChannelsStore();
|
||||
export const communities = new CommunitiesStore();
|
||||
export const gatewayStatus = new GatewayStatusStore();
|
||||
export const messagesStoreProvider = new MessagesStoreProvider();
|
||||
export const userInfoStore = new UserInfoStore();
|
||||
|
@ -741,12 +882,6 @@ export const allStores = {
|
|||
sendMessageAction,
|
||||
};
|
||||
|
||||
selectedChannel.on((newSelectedChannel) => {
|
||||
if (getItem("ui:stateful:presistSelectedChannel")) {
|
||||
setItem("state:openChannelId", newSelectedChannel.id);
|
||||
}
|
||||
});
|
||||
|
||||
unreadStore.subscribe(() => {
|
||||
let totalUnreads = 0;
|
||||
unreadStore.value.forEach(count => totalUnreads += count);
|
||||
|
|
|
@ -34,11 +34,10 @@
|
|||
/* top-level */
|
||||
|
||||
:root {
|
||||
--background-color-0: hsl(180, 11%, 4%);
|
||||
--background-color-1: hsl(180, 11%, 5%);
|
||||
--background-color-2: hsl(180, 11%, 8%);
|
||||
--background-color-3: hsl(180, 11%, 11%);
|
||||
--background-color-4: hsl(180, 11%, 15%);
|
||||
--background-color-0: #151515;
|
||||
--background-color-1: #1a1a1a;
|
||||
--background-color-2: #202020;
|
||||
--background-color-3: #262626;
|
||||
--foreground-color-1: hsl(210, 100%, 100%);
|
||||
--foreground-color-2: hsl(63, 10%, 82%);
|
||||
--foreground-color-3: hsl(63, 2%, 60%);
|
||||
|
@ -144,6 +143,7 @@ body {
|
|||
width: 100%;
|
||||
padding: var(--space-sm);
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.top-bar-heading {
|
||||
|
@ -413,16 +413,22 @@ body {
|
|||
.h5 {font-size: var(--h5)}
|
||||
.text-small {font-size: var(--h6)}
|
||||
.text-bold {font-weight: 700}
|
||||
.text-fg-2 {
|
||||
color: var(--foreground-color-2);
|
||||
}
|
||||
.text-fg-3 {
|
||||
color: var(--foreground-color-3);
|
||||
}
|
||||
|
||||
/* sidebar */
|
||||
|
||||
.sidebar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
background-color: var(--background-color-0);
|
||||
height: 100%;
|
||||
min-width: 255px;
|
||||
max-width: 255px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
|
@ -435,6 +441,13 @@ body {
|
|||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
background-color: var(--background-color-1);
|
||||
}
|
||||
|
||||
.sidebar-buttons {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--space-xs);
|
||||
|
@ -450,7 +463,7 @@ body {
|
|||
align-items: center;
|
||||
justify-content: left;
|
||||
border: none;
|
||||
background-color: var(--background-color-0);
|
||||
background-color: var(--background-color-1);
|
||||
padding: var(--space-xs);
|
||||
margin-bottom: var(--space-xxs);
|
||||
color: var(--foreground-special-color-1);
|
||||
|
|
|
@ -18,6 +18,7 @@ export const query = function(text: string, params: any[] = [], rejectOnError =
|
|||
if (rejectOnError) {
|
||||
reject(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
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,
|
||||
|
||||
UserUpdate = 150,
|
||||
|
||||
CommunityCreate = 160,
|
||||
CommunityUpdate,
|
||||
CommunityDelete,
|
||||
}
|
||||
|
||||
export enum GatewayPresenceStatus {
|
||||
|
|
|
@ -381,23 +381,30 @@ export default function(server: Server) {
|
|||
if ((sessions.length + 1) > MAX_GATEWAY_SESSIONS_PER_USER) {
|
||||
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 = [];
|
||||
sessionsByUserId.set(user.id, sessions);
|
||||
}
|
||||
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, "*");
|
||||
channels.rows.forEach(c => {
|
||||
clientSubscribe(ws, `channel:${c.id}`);
|
||||
});
|
||||
for (let i = 0; i < channels.rows.length; i++) {
|
||||
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.bridgesTo = authData.bridgesTo;
|
||||
|
@ -419,6 +426,7 @@ export default function(server: Server) {
|
|||
d: {
|
||||
user: getPublicUserObject(ws.state.user),
|
||||
channels: channels.rows,
|
||||
communities: communities.rows,
|
||||
presence: getInitialPresenceEntries()
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,13 +10,13 @@ import serverConfig from "../../serverconfig";
|
|||
|
||||
method(
|
||||
"createChannel",
|
||||
[withRegexp(channelNameRegex, string(1, 32))],
|
||||
async (user: User, name: string) => {
|
||||
[withRegexp(channelNameRegex, string(1, 32)), withOptional(uint())],
|
||||
async (user: User, name: string, communityId: number | null) => {
|
||||
if (serverConfig.superuserRequirement.createChannel && !user.is_superuser) {
|
||||
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) {
|
||||
return errors.GOT_NO_DATABASE_DATA;
|
||||
}
|
||||
|
@ -46,23 +46,17 @@ method(
|
|||
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) {
|
||||
return errors.GOT_NO_DATABASE_DATA;
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
id,
|
||||
name,
|
||||
owner_id: permissionCheckResult.rows[0].owner_id
|
||||
};
|
||||
|
||||
dispatch(`channel:${id}`, {
|
||||
t: GatewayPayloadType.ChannelUpdate,
|
||||
d: updatePayload
|
||||
d: result.rows[0]
|
||||
});
|
||||
|
||||
return updatePayload;
|
||||
return result.rows[0];
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -96,7 +90,7 @@ method(
|
|||
"getChannel",
|
||||
[uint()],
|
||||
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) {
|
||||
return errors.NOT_FOUND;
|
||||
}
|
||||
|
@ -109,7 +103,7 @@ method(
|
|||
"getChannels",
|
||||
[],
|
||||
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 : [];
|
||||
}
|
||||
|
|
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";
|
||||
methodGroup(200);
|
||||
import "./apis/channels";
|
||||
methodGroup(300);
|
||||
import "./apis/messages";
|
||||
import { methodNameToId, methods } from "./rpc";
|
||||
methodGroup(400);
|
||||
import "./apis/communities";
|
||||
|
||||
|
||||
console.log("--- begin rpc method map ---")
|
||||
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();
|
||||
let lastMethodId = 0;
|
||||
|
||||
export const methodGroup = (origin: number) => {
|
||||
lastMethodId = origin;
|
||||
}
|
||||
|
||||
export const method = (name: string, args: RPCArgument[], func: ((...args: any[]) => any), requiresAuthentication: boolean = true) => {
|
||||
let id = lastMethodId++;
|
||||
methodNameToId.set(name, id);
|
||||
|
|
|
@ -4,7 +4,7 @@ import rpcRouter from "./routes/api/v1/rpc";
|
|||
import matrixRouter from "./routes/matrix";
|
||||
import { errors } from "./errors";
|
||||
|
||||
const ENABLE_MATRIX_LAYER = false;
|
||||
const ENABLE_MATRIX_LAYER = true;
|
||||
|
||||
export default function(app: Application) {
|
||||
app.use(json());
|
||||
|
|
Loading…
Reference in a new issue