+
{#if $$slots.header}
{/if}
diff --git a/frontend/src/components/overlays/OverlayProvider.svelte b/frontend/src/components/overlays/OverlayProvider.svelte
index ebe2a8d..e4a98d5 100644
--- a/frontend/src/components/overlays/OverlayProvider.svelte
+++ b/frontend/src/components/overlays/OverlayProvider.svelte
@@ -1,5 +1,5 @@
{#each $overlayStore as overlay (overlay.id)}
-
+
{/each}
diff --git a/frontend/src/components/overlays/Prompt.svelte b/frontend/src/components/overlays/Prompt.svelte
index 586251a..2fa89e2 100644
--- a/frontend/src/components/overlays/Prompt.svelte
+++ b/frontend/src/components/overlays/Prompt.svelte
@@ -30,7 +30,6 @@
-
diff --git a/frontend/src/permissions.js b/frontend/src/permissions.js
new file mode 100644
index 0000000..7fc795d
--- /dev/null
+++ b/frontend/src/permissions.js
@@ -0,0 +1,49 @@
+export const CapabilityGrant = {
+ None: 0,
+ ResourceOwner: 1,
+ ResourceManager: 2,
+ GlobalSuperuser: 3
+};
+
+export const CapabilityType = {
+ None: 0,
+ ManageChannel: 1,
+ ManageCommunity: 2,
+ ManageMessage: 3
+};
+
+
+export function getGrantFor(user, capability, resource) {
+ let resourceOwnerId = -1;
+
+ switch (capability) {
+ case CapabilityType.ManageChannel: {
+ resourceOwnerId = resource.owner_id;
+ break;
+ }
+
+ case CapabilityType.ManageCommunity: {
+ resourceOwnerId = resource.owner_id;
+ break;
+ }
+
+ case CapabilityType.ManageMessage: {
+ resourceOwnerId = resource.author_id;
+ break;
+ }
+
+ default: {
+ return CapabilityGrant.None;
+ }
+ }
+
+ if (user && user.id === resourceOwnerId) {
+ return CapabilityGrant.ResourceOwner;
+ }
+
+ if (user && user.is_superuser) {
+ return CapabilityGrant.GlobalSuperuser;
+ }
+
+ return CapabilityGrant.None;
+}
diff --git a/frontend/src/request.js b/frontend/src/request.js
index 446c095..15ab5b6 100644
--- a/frontend/src/request.js
+++ b/frontend/src/request.js
@@ -10,6 +10,7 @@ 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),
diff --git a/frontend/src/responsive.js b/frontend/src/responsive.js
index 3bb497e..26cde94 100644
--- a/frontend/src/responsive.js
+++ b/frontend/src/responsive.js
@@ -1,5 +1,5 @@
import { getItem } from "./storage";
-import { showSidebar, smallViewport, theme, usesKeyboardNavigation } from "./stores";
+import { showSidebar, smallViewport, theme, usesKeyboardNavigation, overlayStore } from "./stores";
function initViewportSizeHandler() {
const root = document.querySelector(':root');
@@ -33,22 +33,30 @@ function updateTheme(themeName) {
classes.add(`theme--${themeName}`);
}
-function initKeyboardNavigationDetection() {
+export function initResponsiveHandlers() {
+ // Keyboard navigation detection
document.addEventListener("keydown", ({ key }) => {
if (key === "Tab") {
usesKeyboardNavigation.set(true);
}
});
- document.addEventListener("click", e => {
+ const keyboardClickHandler = (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)');
diff --git a/frontend/src/stores.js b/frontend/src/stores.js
index d992415..8d91fa6 100644
--- a/frontend/src/stores.js
+++ b/frontend/src/stores.js
@@ -1,5 +1,6 @@
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";
@@ -236,7 +237,7 @@ class MessageStore extends Store {
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
message._mentions = true;
}
- if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) {
+ if (getGrantFor(userInfoStore.value, CapabilityType.ManageMessage, message)) {
message._editable = true;
}
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) {
@@ -409,10 +410,31 @@ class OverlayStore extends Store {
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);
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index f544a2b..47cf2f7 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -178,6 +178,15 @@ 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;
@@ -193,6 +202,8 @@ body {
}
.modal-header {
+ display: flex;
+ align-items: center;
font-weight: 650;
padding: var(--space-md);
}
@@ -210,6 +221,21 @@ body {
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 {
+ background-color: transparent;
+ padding-top: var(--space-xxs);
+}
+
.modal-primary-action {
float: right;
}
@@ -272,6 +298,21 @@ 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:hover {
+ color: var(--foreground-color-0);
+}
+
.button {
color: var(--foreground-color-1);
background: none;
@@ -283,7 +324,7 @@ body {
border-radius: 9999px;
font: inherit;
user-select: none;
- font-weight: 550;
+ font-weight: 600;
}
.button:hover {
@@ -292,34 +333,25 @@ body {
.button-accent {
color: var(--colored-element-text-color);
- background-color: var(--purple-2);
-}
-
-.button-accent:hover {
background-color: var(--purple-1);
}
-.button-accent:disabled {
+.button-accent:hover {
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-accent:disabled {
+ background-color: var(--purple-1);
}
.button-danger {
color: var(--red-2);
}
+.button-danger:hover {
+ color: var(--red-1);
+}
+
/* icon buttons */
.icon-button {
@@ -531,7 +563,11 @@ body {
border-radius: 9999px;
font-size: x-small;
margin-left: var(--space-sm);
- cursor: pointer;
+}
+
+.user-badge.secondary {
+ background-color: var(--background-color-1);
+ border: 1px solid var(--background-color-2);
}
/* util */
diff --git a/src/rpc/apis/users.ts b/src/rpc/apis/users.ts
index 3589c90..9161bde 100644
--- a/src/rpc/apis/users.ts
+++ b/src/rpc/apis/users.ts
@@ -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, usernameRegex, withRegexp } from "./../rpc";
+import { RPCContext, bufferSlice, method, methodButWarningDoesNotAuthenticate, string, uint, usernameRegex, withRegexp } from "./../rpc";
import sharp from "sharp";
import { randomBytes } from "crypto";
import { unlink } from "fs/promises";
@@ -165,3 +165,19 @@ 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]);
+ }
+);